Skip to content

fix: persist Claude Agent SDK lifecycle across turns - #11

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

fix: persist Claude Agent SDK lifecycle across turns#11
mradwankhalil wants to merge 11 commits into
openchamber:mainfrom
mradwankhalil:fix/compaction-usage-and-summary

Conversation

@mradwankhalil

@mradwankhalil mradwankhalil commented Sep 3, 2026

Copy link
Copy Markdown

Summary

  • reuse one Claude Agent SDK Query and MCP lifecycle across normal conversation turns
  • preserve queued SDK input and parked bridge state across tool continuations
  • reset lifecycle at summary, model, working-directory, and compaction boundaries
  • retain per-turn usage accounting and summary-request classification fixes
  • add Windows Claude CLI resolution coverage and regression tests
  • mark the patched checkout as 0.14.0-fix.1

Why

Creating a fresh Agent SDK lifecycle for every normal turn can discard the SDK/MCP prompt cache and resend the conversation unnecessarily, contributing to inflated usage. This keeps the normal conversation lifecycle persistent while retaining explicit reset boundaries for summaries, model/cwd changes, and compaction.

Verification

  • bun run test passed
  • bun run build passed
  • git diff --check passed
  • disposable OpenCode startup from the relocated checkout served /health with expected HTTP 401 and no stderr
  • active local checkout is configured at C:\Users\Zephyrus\Documents\ai\opencode-claude

Notes

  • The live authenticated Claude five-hour usage meter was not exercised to avoid consuming quota.
  • This PR does not change provider routing or OpenCode/OMO fallback configuration.

Update: bridge teardown was on the response path

input.close() tears the SDK stream down synchronously, so calling it inline meant
every turn paid to close the previous turn's bridge before it could proceed.
Measured on Windows with Opus 5: ~11s for a trivial prompt, ~3s once deferred.

closeInputDeferred() moves that one call to the next macrotask at both teardown
sites — deleteBridge, and putBridge superseding an earlier turn for the same
conversation. Everything else stays synchronous: handle.close(), pending-tool
rejection, and removal from the pool. The stream still closes, so the leak this
branch fixes stays fixed; it just no longer happens while a request is waiting.

test/bridge-teardown-regression.ts covers both paths — asserts input.close()
has not run immediately after teardown, that handle.close() has, that the bridge
leaves the pool at once, and that the close does still happen one tick later.
Wired into npm test; all six suites pass.

@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
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