Skip to content

fix: relay tool-result media + harden persistent Claude SDK stream (batch) - #11

Open
mradwankhalil wants to merge 18 commits into
openchamber:mainfrom
mradwankhalil:fix/compaction-usage-and-summary
Open

mradwankhalil wants to merge 18 commits into
openchamber:mainfrom
mradwankhalil:fix/compaction-usage-and-summary

Conversation

@mradwankhalil

@mradwankhalil mradwankhalil commented Sep 3, 2026

Copy link
Copy Markdown

Tool-result media fix + local hardening batch

Headline (this push): tool-result images now reach Claude

Problem - on the claude-code provider, images returned by tools (e.g. the read tool on a PNG) never reached the model; it only saw "Image read successfully". Directly attached images were unaffected.

Root cause - OpenCode cannot place media inside tool results for openai-compatible providers, so it promotes tool media into a synthetic user message: "Attached media from tool result:" (sst/opencode packages/opencode/src/session/message-v2.ts; supportsMediaInToolResult returns false for @ai-sdk/openai-compatible). In the parked-bridge (MCP) flow this plugin resumes the turn by resolving the parked tool call only - the synthetic message was ignored, so the media was dropped on the floor.

Fix -

  • collectToolResults now captures tool-result image parts ({ text, attachments }) when a transport does include them.
  • collectSyntheticToolMedia detects OpenCode’s promoted synthetic message and merges its images into the single resolved tool call.
  • The parked MCP reply now returns image content blocks alongside text.

Evidence (claude-code/opus through this proxy)

  • read on a generated PNG -> "The image says SQUEEZ-TEST-7-3-9. It’s orange monospace text on a dark background."
  • read on a generated JPEG -> "JPEG-TEST-4-2-0 ... cyan monospace text on a dark navy background".
  • Adapter debug log: [opencode-claude] attached promoted tool-result media {"count":1} immediately before resuming parked bridge {"resolved":1}.

Regressions checked - plain-text reply (PONG), bash tool call, text-file read: all pass. Repo suite: npm test (12 bun regression files) green.

Limitations - images only (PDFs in tool results stay text); remote http(s) image URLs are skipped; if a single request resolves multiple parked tools the promoted media cannot be mapped and is skipped with a warning; the history-transcript path (non-resumable sessions) is unchanged.

Also in this batch (previously local)

  • de6c815 declare input window for 1M models so auto-compaction fires before the edge
  • 55112db / 9584142 / 5f600b8 persistent Claude SDK Query + prompt input queue
  • 8c24d64 serialize persistent response pumps; 0b1aa2f serialize SDK stream pulls
  • 00b87c9 harden bridge teardown; d0cb807 defer input.close() off the response path
  • 0c89d3a resolve Windows Claude CLI fallbacks
  • f858a34 reset Claude session after compaction; 994db2b classify summary updates as metadata; 29ffae0 per-turn usage across compaction continuations
  • f6fe917 / 0865ab1 / ae1a42e regression coverage for lifecycle, continuation cancellation, Query lifecycle

@mradwankhalil

Copy link
Copy Markdown
Author

Follow-up fix added after live validation. /compact was lowering OpenCode's local context correctly, but the next normal request resumed the old sticky Claude session, whose hidden pre-compaction context reported ~806k cached input tokens. b4d999 adds the regression; 858a34 clears the base conversation binding at a classified summary boundary so the next turn transfers compacted OpenCode history into a fresh Claude session. Focused regressions, typecheck, build, and diagnostics pass. The full suite still stops at the unchanged Windows smoke assertion in test/smoke.ts:236.

Introduce ClaudePromptInput, a long-lived AsyncIterable<SDKUserMessage>
backed by an unbounded queue. Each push() resolves the next pending
pull; close() releases any waiting consumer. This is the primitive the
proxy uses to stream user input into a persistent Agent SDK Query.
@mradwankhalil mradwankhalil changed the title fix: keep compaction usage per-turn and classify summary updates fix: persist Claude Agent SDK lifecycle across turns Sep 6, 2026
Closing the prompt input tears the SDK stream down synchronously. Doing it
inline meant every turn paid to close the previous turn's bridge before it
could proceed, adding seconds to every request. Measured on Windows with
Opus 5: ~11s for a trivial prompt, ~3s once deferred.

The close still happens, so the stream leak this branch fixes stays fixed —
it just runs on the next macrotask instead of while a request is waiting.
handle.close() and removal from the pool remain synchronous.

Adds test/bridge-teardown-regression.ts covering both teardown paths:
deleteBridge, and putBridge superseding an earlier turn for the same
conversation.
@mradwankhalil
mradwankhalil force-pushed the fix/compaction-usage-and-summary branch from f16b23d to d0cb807 Compare September 6, 2026 18:13
@mradwankhalil mradwankhalil changed the title fix: persist Claude Agent SDK lifecycle across turns fix: prevent persistent Claude SDK stream hangs Sep 12, 2026
OpenCode schedules automatic compaction at (limit.input - reserved)
when a model declares an input cap, falling back to (context - maxOutput)
otherwise. The 1M Claude Code models declared only context/output, so
scheduled compaction landed at the very edge of the window (~98%
observed in long sessions). Declaring a 900k input window moves the
trigger to ~88-90% and leaves 200K models unchanged.
OpenCode cannot carry media inside tool results for openai-compatible
providers (the claude-code provider), so it promotes tool media — e.g.
`read` on an image — into a synthetic user message
("Attached media from tool result:"). In the parked-bridge path that
message was dropped: the turn resumed by resolving the parked MCP call
with text only, so Claude never saw the image.

- collectToolResults returns {text, attachments}; image parts are
  extracted when a transport does carry them inside the tool result.
- collectSyntheticToolMedia finds OpenCode’s promoted synthetic
  message; its images merge into the single resolved tool call payload.
- Parked MCP replies now emit `image` content blocks next to `text`.

Verified on OpenCode + claude-code/opus:
- `read` on a PNG → model reports the exact image text.
- `read` on a JPEG → same.
- plain text, bash tool, and text-file reads unchanged.
- repo test suite (bun, 12 regression files) green.
@mradwankhalil mradwankhalil changed the title fix: prevent persistent Claude SDK stream hangs fix: relay tool-result media + harden persistent Claude SDK stream (batch) Sep 16, 2026
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