fix(acp): settle deferred terminal frames - #4192
Conversation
A terminal that arrived before prompt acknowledgement could lose event-owned correlation during replay, leaving session/prompt pending forever. Shared strict derivation now preserves ownership, dropped terminals are observable, and unrelated traffic cannot indefinitely extend the turn watchdog. Lore-id: a4190cde Constraint: terminal ownership must require exact commandId and turnId correlation Rejected: loosen correlation matching | stale or foreign turns could settle the active prompt Confidence: high Scope-risk: narrow Reversibility: easy Tested: 95 ACP tests; coding-agent check; repository lint
Yeachan-Heo
left a comment
There was a problem hiding this comment.
P1 — pre-ack watchdog rejection loses ownership before the late acknowledgement
Reviewed exact head 39c64b2d840a3365a0c6fc272e7ac576907fe018. The deferred-terminal fix is directionally correct, but this head does not cover the newly confirmed pre-ack watchdog ownership sequence.
Causal gap
prompt()waits forrecord.adapter.prompt()atacp-agent.ts:1461-1466. If the watchdog expires first,#rejectPromptclearsrecord.activePromptand settles the waiter at:2344-2366; because acknowledgement has not arrived,waiter.correlationis empty and nosettledPromptCorrelationstombstone is recorded.- When the late acknowledgement eventually arrives,
:1472-1475assigns its exactcommandId/turnId, but there is no retired-waiter branch that installs that now-authoritative identity intosettledPromptCorrelationsbefore replay/return. - After successor prompt B is acknowledged, fully correlated nonterminal A frames are not fenced to B's exact correlation. At
:2208-2268, A is absent fromsettledPromptCorrelations;tool_execution_startcan populaterecord.toolArgsat:2249-2256, and A wire updates can reach publication. The exact terminal fence does not protect nonterminal message/tool traffic.
Required focused regression
Add one virtual-clock test with this exact order:
- Dispatch prompt A and withhold its SDK acknowledgement.
- Expire A's prompt watchdog while
waiter.correlationis still empty; assert the watchdog rejection occurred. - Deliver A's late accepted acknowledgement carrying complete exact
commandId+turnId; assert A rejects to the ACP caller and that identity is retired/tombstoned. - Start and acknowledge prompt B.
- Deliver fully correlated A
message_end/message_updateandtool_execution_startframes. - Assert B receives no A message/tool publication, no tool/activity mutation, and no watchdog refresh/rearm.
- Deliver a deliberately correlationless session event; assert it remains publishable but still does not refresh B's watchdog.
- Include private sentinel text in A message/tool payloads and assert any diagnostic metadata contains neither sentinel nor private payload data.
The smallest acceptable fix is to keep the rejected waiter observable until a late acknowledgement can install its exact correlation tombstone, then fail closed for complete nonterminal correlations that do not exactly match acknowledged B. Do not suppress deliberately correlationless session events.
Current hosted CI does not exercise this sequence, so green checks cannot override the ownership hole.
Signed-off-by: Yeachan Heo bellman@gajae-code.dev
VERDICT: REQUEST_CHANGES
Summary
Correlation rationale
Terminal ownership still requires the complete
commandId+turnIdidentity fence. The fix restores the missing correlation inputs during deferred replay rather than weakening matching. Terminal events usestrictCorrelationFrom(frame, event)through the same helper as live ingest; nonterminal routing retainscorrelationFrom, while watchdog attribution separately rejects conflicting envelope/event claims because those frames must not mutate or extend another turn's bound.Watchdog reconciliation
Unrelated traffic proves the session host process is alive, but it does not prove progress on this prompt. Only exact prompt-owned frames now restart the per-gap watchdog and update tool/model activity. This preserves long inference/tool protection when the host is demonstrably working on the active turn while keeping a finite bound on a wedged turn amid unrelated host traffic.
Tests
devwith a frozen virtual clock (Timed out waiting for pre-acknowledgement prompt completion)bun test packages/coding-agent/test/acp-prompt-settle-on-completion.test.ts packages/coding-agent/test/acp-prompt-watchdog.test.ts packages/coding-agent/test/sdk-acp-prompt-terminal.test.ts packages/coding-agent/test/acp-prompt-conformance.test.ts packages/coding-agent/test/acp-session-reconnect.test.ts packages/coding-agent/test/sdk-acp-two-client-race.test.ts packages/coding-agent/test/sdk-acp-production-path.test.ts packages/coding-agent/test/sdk-acp-adapter.test.ts packages/coding-agent/test/acp-event-mapper.test.ts— 95 passbun --cwd=packages/coding-agent run check— passbun run lint— passReview
Follow-up
The unrelated startup warning was confirmed out of scope: the settings schema contains
task.enableLspbut no baretask.enable. This PR does not change settings reconciliation.Fixes #4190