Skip to content

fix(agent-sdk): show local command replies whose response never streamed - #444

Open
ayush5harma wants to merge 2 commits into
srothgan:mainfrom
ayush5harma:contrib/local-command-replies
Open

ayush5harma wants to merge 2 commits into
srothgan:mainfrom
ayush5harma:contrib/local-command-replies

Conversation

@ayush5harma

@ayush5harma ayush5harma commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary

  • Claude Code answers a command it runs locally (/rename, /color, /usage, ...) with one complete top-level assistant message, and that message's API response never streams. The bridge now records the response ID of the last top-level message_start. When a completed top-level message carries a response ID that never started a stream, the bridge shows its text once.
  • Streamed replies are unchanged: they are still shown from their deltas. Subagent frames, error frames and messages without a response ID stay out of the transcript.
  • The rule reads only typed SDK fields (parent_tool_use_id, error, the message ID). It does not use the undeclared local_command_* fields.
  • The existing check on the declared context_usage field stays. A test shows that /context Markdown is still shown even when a message has no response ID.
  • Docs: one sentence in the architecture's activity section on where reply text comes from, and one line on the commands page.

Why

On main, /rename probe, /color blue and /usage run in Claude Code, but nothing appears under the command in claude-rs. The reply exists only in that unstreamed message, which the bridge drops. This is the reply half of #438.

The SDK declares SDKLocalCommandOutputMessage (system/local_command_output), and the bridge already shows it. I checked whether Claude Code uses it for these commands. On Claude Code 2.1.288, the version the pinned SDK 0.3.288 bundles, /rename, /color, /usage and /context each produced exactly one complete assistant message, zero stream events and no local_command_output message. So the declared message never carries these replies, and the unstreamed assistant message is their only carrier.

Refs #438

Validation

  • Automated, on this branch (macOS 27.2, Apple Silicon, rustc 1.98.1 from nixpkgs rather than the CI toolchain):

    • cargo fmt --all -- --check, cargo fetch --locked: pass
    • cargo clippy --locked --all-targets --all-features -- -D warnings: pass. Five lints that are new in clippy 1.98 are allowed, because main fails them too: collapsible_match, manual_is_multiple_of, manual_is_variant_and, map_unwrap_or, while_let_loop.
    • cargo test --locked --all-features: pass. The serial terminal_resize run: pass.
    • agent-sdk: npm run build, test, lint, knip, audit and quality:duplicates: pass. The new tests live in src/bridge/message_handlers.test.ts, so npm test runs them (see Notes). They cover:
      • a never-streamed reply shown once;
      • streamed, subagent, empty, error and uncorrelated replies not shown again;
      • a non-streaming fallback after a partial stream;
      • context usage without a response ID.
  • Manual: I drove the debug binary in tmux against the Claude Code bundled with the pinned SDK (2.1.288), running /rename probe and then /color blue.

  • Screen text, main:

    User
    /rename probe-baseline
    User
    /color blue
     ❯ Type a message...
    

    This branch:

    User
    /rename probe-up-replies
    Claude
    Session renamed to: probe-up-replies
    Elapsed 0.0s
    User
    /color blue
    Claude
    Session color set to: blue
    Elapsed 0.0s
     ❯ Type a message...
    

Notes

  • Breaking changes: N/A
  • Docs updated: docs/src/architecture.md and docs/src/commands.md.
  • Behaviour change worth knowing: any complete top-level reply that never streamed is now shown. That includes the text of Claude Code's non-streaming fallback after a broken stream, which main drops. The partial deltas shown before the break stay above it; a test covers this.
  • The first commit's body says "measured on 2.1.296". The behaviour is identical on 2.1.288, and the test comment now cites 2.1.288. If you squash, please drop that version from the message.
  • Unrelated finding: npm test runs node --test dist/**/*.test.js under sh, where ** is a single *. As a result, dist/bridge.test.js and dist/bridge.contract.test.js never run, locally or in CI. That is 333 tests. Run by hand, the same 16 spawned-bridge tests fail on main and on this branch. Reported separately as [Bug]: npm test skips bridge.test.js and bridge.contract.test.js, where 16 tests fail #446.
  • No SDK upgrade: this uses only what the pinned @anthropic-ai/claude-agent-sdk 0.3.288 already exposes.
  • Governance/release impact: N/A

- record the API response ID of the last top-level stream that began, so a
  completed assistant message can be correlated with a streamed response
- show a complete top-level reply once when its response never streamed:
  Claude Code answers /rename, /color and /usage that way (measured on
  2.1.296), so its text had no other carrier (srothgan#438)
- keep SDK-owned context Markdown as before; subagent, error and
  uncorrelated messages stay out of the transcript
- cover both directions in a bridge test that npm test runs, and describe
  the rule in the architecture's activity section and the commands page
… version

- cite Claude Code 2.1.288, the version the pinned SDK bundles, where the
  reply shape was measured; it sends no system/local_command_output for
  /rename, /color, /usage or /context
- show that a non-streaming fallback after a partial stream is shown in
  full under its own response ID, beside the partial deltas
- show that SDK context usage Markdown still appears without a response
  ID, which is why the declared context_usage check stays
@ayush5harma

Copy link
Copy Markdown
Author

Hi @srothgan, this is ready for review whenever you have time.

Before opening it I read CONTRIBUTING.md, AGENTS.md and docs/src/architecture.md, and applied your feedback from #443:

  • The bridge decides from typed SDK fields only: parent_tool_use_id, error and the API response ID. It uses no undeclared local_command_* fields and nothing internal to Claude Code.
  • The rule sits in the activity owner, next to the existing response-ID correlation. The architecture doc's activity section says where reply text comes from.
  • CHANGELOG is left to you, and there is no agent attribution in the commits.
  • There is no SDK upgrade. Everything here uses what the pinned @anthropic-ai/claude-agent-sdk 0.3.288 already exposes, and I measured the behaviour on the Claude Code 2.1.288 it bundles.

After opening, I added one commit from my own review:

  • The test comment now cites 2.1.288 instead of 2.1.296.
  • A test covers a non-streaming fallback after a partial stream.
  • A test covers /context Markdown arriving without a response ID, which is why the declared context_usage check stays.

The PR description now explains why the declared local_command_output message doesn't cover these commands, and has before/after screen text. I also reported the unrelated npm test glob problem as #446.

I couldn't apply labels. Per CONTRIBUTING, I think type: fix and area: agent-sdk fit.

Could you check it on your end and merge it if it looks good? Happy to change anything you'd like done differently.

ayush5harma added a commit to ayush5harma/claude-code-rust that referenced this pull request Oct 10, 2026
…s-prs

chore: make the fork exactly upstream plus PRs srothgan#444 and srothgan#445

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant