fix(runtime): drain post-success session updates before closing promp…#251
Merged
steipete merged 2 commits intoApr 25, 2026
Merged
Conversation
Contributor
|
Landed via maintainer fixup and rebase onto main.
Thanks @logofet85-ai! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
runPromptTurnclosed the prompt turn immediately afterclient.prompt()resolved on the success path. If the underlying adapter produced any lateassistant_delta/tool_call/tool_call_updateshortly after the RPC response, the wrapper had already synthesized the turn closure and those updates were dropped, never reaching.acp-stream.jsonlor downstream consumers.client.waitForSessionUpdatesIdle({ idleMs: 1000, timeoutMs: 5000 }). The drain is opt-in (no-op if the client does not implement it) and fully capped by the existingSESSION_REPLY_IDLE_MS/SESSION_REPLY_DRAIN_TIMEOUT_MSconstants.stopReason/sourcesemantics, conversation-model behavior, cancel path, error propagation, and every other caller contract are untouched. No new exports, no signature changes, no new configuration.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
src/runtime/engine/prompt-turn.tsreturned immediately afterwithTimeout(promptPromise, ...)resolved, while the catch/timeout path already drained late session updates viawaitForSessionUpdatesIdle. The asymmetry meant any lateassistant_delta/tool_call/tool_call_updatearriving after a successful RPC response was discarded because the turn was closed before it could be observed.acp.v1.session.update.terminationconformance case uses a mock agent which emits all updates synchronously before returningstopReason, so the mock path never exercised the race.tool_useon a later tick than the one that resolvessession/prompt.Regression Test Plan (if applicable)
test/integration.test.tsrunPromptTurn: post-success drain runs before closing the turn— asserts the drain is invoked withidleMs=1000, timeoutMs=5000before the return on the RPC-success path.runPromptTurn: late session updates after successful prompt reach the drain— asserts a late async update emitted during the drain window is observed before the turn closes.runPromptTurn: missing waitForSessionUpdatesIdle still returns cleanly on success— asserts backward compatibility with clients that do not implement the optional drain method.runPromptTurnand the adapter client interface; asserting it at the seam avoids depending on end-to-end adapter behavior while still locking the exact invariant.acp.v1.session.update.termination) does not exercise late post-success updates because the bundled mock agent emits updates synchronously before resolvingsession/prompt.User-visible / Behavior Changes
session/promptresolves. Lateassistant_delta,tool_call, andtool_call_updateevents now reach the stream before the turn is closed.Diagram (if applicable)
Security Impact (required)
Repro + Verification
Environment
tsx test/mock-agent.tsfor conformance + unit-level mock client for the new seam testsacp-core-v1)Steps
pnpm installpnpm buildpnpm build:testnode --test --test-name-pattern="post-success drain|late session updates after successful prompt|missing waitForSessionUpdatesIdle" dist-test/test/integration.test.jspnpm run conformance:run -- --format json --report ./conformance-artifacts/update-termination-after.json --case acp.v1.session.update.terminationExpected
runPromptTurn:tests pass.acp.v1.session.update.terminationcontinues to pass ({ cases: 1, passed: 1, failed: 0 }).Actual (v0.5.3 baseline, before patch)
runPromptTurn: post-success drain runs before closing the turn— FAIL (expected ['prompt','drain(1000/5000)'], actual['prompt']).runPromptTurn: late session updates after successful prompt reach the drain— FAIL (lateUpdateEmitted === false).runPromptTurn: missing waitForSessionUpdatesIdle still returns cleanly on success— PASS (backward compat preserved).Actual (after patch)
cases: 1, passed: 1, failed: 0, ~250–280 ms).Evidence
Trace snippet (real Claude ACP stream, anonymized, before patch):
After patch (local run, copy-pasted from terminal):
Human Verification (required)
waitForSessionUpdatesIdle(backward compat).acp.v1.session.update.terminationconformance case)..catch(() => {})so it cannot turn a successful prompt into an error — confirmed manually by running the seam test with a rejectingwaitForSessionUpdatesIdlestub.waitForSessionUpdatesIdle— confirmed by the dedicated seam testrunPromptTurn: missing waitForSessionUpdatesIdle still returns cleanly on success.Review Conversations
Compatibility / Migration
Risks and Mitigations
SESSION_REPLY_DRAIN_TIMEOUT_MS(5 s) as an upper bound; beyond that the turn closes regardless.waitForSessionUpdatesIdlemight be surprised by a new call on the success path.?.) and all callers continue to compile; missing-method path is covered by a dedicated test.AI Assistance
pnpm build,pnpm build:test, three new seam tests (before + after), and the baselineacp.v1.session.update.terminationconformance case (before + after). No real-agent run was performed inside this PR.