From fc8d339bf04cf88a8d83e3a4e9906c542e212003 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Sun, 26 Jul 2026 14:29:30 +0800 Subject: [PATCH] fix(acp): settle admission-blocked and retried prompts after the merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The automation bridge inherited two master-era assumptions the message machine no longer honors. A prompt blocked at pre-turn admission opens no turn, so no turn/end could ever settle it — the bridge now watches whenIdle() and reports a turnless slot as cancelled (the disposed-agent guard moved to a registry identity check before send). A failed turn no longer rejects at its turn/end either: agent.retry() closes the failed turn and opens a successor on the same history, so the bridge holds the terminal error and lets a retry-triggered turn/start adopt the prompt, rejecting only at quiescence with no successor. Also: refresh the empty-response-retry fixture for retry-as-turn logging, adapt master-side tests to the unified send()/UserMessageData API and registry-fact disposal, resync the doc pairs both sides touched, trim architecture.md back under its word ceiling, and regenerate the event and persistence catalogs. --- .../feature/2026-06-30-hook-bridges.i18n.yaml | 4 +- .../2026-06-30-interception-seams.i18n.yaml | 4 +- .../feature/2026-07-06-sandbox.i18n.yaml | 4 +- ...26-07-19-model-facing-goal-tools.i18n.yaml | 4 +- ...9-same-session-goal-round-driver.i18n.yaml | 4 +- ...07-02-remove-stream-chunk-mirror.i18n.yaml | 4 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 5 +- docs/architecture.zh.md | 5 +- docs/cookbook/adding-a-tool.i18n.yaml | 4 +- docs/cookbook/extension-cookbook.i18n.yaml | 4 +- .../llm-streaming.i18n.yaml | 4 +- docs/core-data-structures/session.i18n.yaml | 4 +- docs/core-data-structures/tools.i18n.yaml | 4 +- docs/event-producer-consumer.md | 45 +++++++------- docs/persistence-catalog.md | 58 ++++++------------- .../empty-response-retry/session.jsonl | 20 ++++--- packages/acp/acp/src/index.ts | 50 ++++++++++++---- 18 files changed, 119 insertions(+), 112 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml index 1ad47cd1f6..dc44bbad28 100644 --- a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-06-30-hook-bridges.md: c207b6901155925215548676364e903f5de2f29b -2026-06-30-hook-bridges.zh.md: d396279d7ed1991536da2cea39e2aec5e50960c2 +2026-06-30-hook-bridges.md: 04cbef6738bcc1d0f6ac6a395ce4abecdb16b984 +2026-06-30-hook-bridges.zh.md: 60146edcbab8098d52682d77e3f57c6d2676f291 diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml b/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml index ff40078df0..0d5d8440bd 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-06-30-interception-seams.md: 81799f4c6e3e7a4c6b9605cd97f5728b99d11995 -2026-06-30-interception-seams.zh.md: 65ae16842c632641e7ac65908162f4784dc6e1e0 +2026-06-30-interception-seams.md: 9c293c273e2c14f168ef5a7e7fa70f06f82955ac +2026-06-30-interception-seams.zh.md: 39eb68983dfcfc4f2d77dc709880167c72469f83 diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml index 6437d7813d..3be3b71b6f 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-06-sandbox.md: 723ef170188dc11da24e049a1e2838fb240d0a17 -2026-07-06-sandbox.zh.md: a8c7743bb3d499fb58f507ea2c202b44efe2311d +2026-07-06-sandbox.md: 1200b2660619fd2756874e0b9106d10da8c0c129 +2026-07-06-sandbox.zh.md: b6a9b831d17abbce1f1cb3a4ca545ecfb1a3574e diff --git a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml index 9beb453252..6cf16c04ad 100644 --- a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-model-facing-goal-tools.md: 2c53a7658e213dee4fecc93709244f97b821aca0 -2026-07-19-model-facing-goal-tools.zh.md: aa7b5ea14b7afff88819f0efa35c5f2d5e2e933e +2026-07-19-model-facing-goal-tools.md: bc4305af80bb13ceeff1888d489dcd8a00132f94 +2026-07-19-model-facing-goal-tools.zh.md: b07f62aa526902c4b2e9c081777a76ca53783d31 diff --git a/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml index 8f948d3d45..b354999813 100644 --- a/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-same-session-goal-round-driver.md: d23af9a9b05d60d2dccad095455524844f1185b9 -2026-07-19-same-session-goal-round-driver.zh.md: f4f0cd6fd575d14427025bdbd8d10bc90e25f780 +2026-07-19-same-session-goal-round-driver.md: 0e6be9fe3109336d47867ab52c585dc267309fb4 +2026-07-19-same-session-goal-round-driver.zh.md: cfd9d1aa8cbc3c17cd046df4f57a8f79a6877f5c diff --git a/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml index 3a0fde3d06..5bc1c28d52 100644 --- a/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-02-remove-stream-chunk-mirror.md: 8b26589a9e89f83d631fa98e801a8d3e08e105d0 -2026-07-02-remove-stream-chunk-mirror.zh.md: fcd8c53b8b1b2a5b81f9f929ffd9e06ff6128e45 +2026-07-02-remove-stream-chunk-mirror.md: 5b8e4a33819af69b68a9c5f93be12f4069baebde +2026-07-02-remove-stream-chunk-mirror.zh.md: 9357748884b0f3857e75d8fcc3b28fc5c5db1838 diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 1798b46908..3aa0f663b5 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -architecture.md: b426891c0483f42a64b597632cf1871aff79ca2d -architecture.zh.md: 13feefb6854e79ddee38602902d325a789fd7744 +architecture.md: 456b97da984c39cfbd3284d31d81d115ecd00a3b +architecture.zh.md: e1aefb596483978f287b05ba446dc0d642a042c8 diff --git a/docs/architecture.md b/docs/architecture.md index 3bac939525..456b97da98 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -100,9 +100,8 @@ forever: each model-order result -> ordered tools/post-execute -> 'tool/result' drain accepted tool context and steering 'step/end' - a concluding tool result ends the turn; drained steering waits for the next turn - continue when tools or steering require another step - otherwise agent/stopping -> drain once more -> continue only for steering + continue for tools or steering unless a result concluded the turn + otherwise agent/stopping -> drain -> continue only for steering 'turn/end' -> agent/idle start the next waking queued message, or emit agent/status(idle) diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index f7258a2fcb..e1aefb5964 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -100,9 +100,8 @@ forever: each model-order result -> ordered tools/post-execute -> 'tool/result' drain accepted tool context and steering 'step/end' - a concluding tool result ends the turn; drained steering waits for the next turn - continue when tools or steering require another step - otherwise agent/stopping -> drain once more -> continue only for steering + continue for tools or steering unless a result concluded the turn + otherwise agent/stopping -> drain -> continue only for steering 'turn/end' -> agent/idle start the next waking queued message, or emit agent/status(idle) diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml index 4c0e480bee..423737be39 100644 --- a/docs/cookbook/adding-a-tool.i18n.yaml +++ b/docs/cookbook/adding-a-tool.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -adding-a-tool.md: c4deed8e13afcdc8e1a714364b086b8b0da018c1 -adding-a-tool.zh.md: 103c92b596a90192aae5a6b4d1e42fbcf052cf66 +adding-a-tool.md: d06e3d8e3c7da1f71a55bf9c4f56cd4b2cc03697 +adding-a-tool.zh.md: 53f608eba3b26b124f873990fa13ce1572c0baf2 diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml index a7e63abdeb..dc1c57ee93 100644 --- a/docs/cookbook/extension-cookbook.i18n.yaml +++ b/docs/cookbook/extension-cookbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -extension-cookbook.md: 0ab337377518f832c0649bc80cf2941751cb934f -extension-cookbook.zh.md: f7c2572d0b91867589636249f29896848f8aec82 +extension-cookbook.md: 3ef9e6e7d7117d76f20da274a0f1d689497df441 +extension-cookbook.zh.md: 575c8d5e543fe578feb925cec41f6704dbc2797c diff --git a/docs/core-data-structures/llm-streaming.i18n.yaml b/docs/core-data-structures/llm-streaming.i18n.yaml index 5de51d2794..2004711790 100644 --- a/docs/core-data-structures/llm-streaming.i18n.yaml +++ b/docs/core-data-structures/llm-streaming.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -llm-streaming.md: fb97e74a9ec01e62ba940295112fb157cc73bd1d -llm-streaming.zh.md: fda59a64aeef1c037372d69dbc15ee0de48de222 +llm-streaming.md: 0b2c81b1cb264a949cf3c1a1a22f45633cddab65 +llm-streaming.zh.md: a9391855ae170dd5f5b1a163cb0b586e15be41ea diff --git a/docs/core-data-structures/session.i18n.yaml b/docs/core-data-structures/session.i18n.yaml index 454bd17c38..efe3964084 100644 --- a/docs/core-data-structures/session.i18n.yaml +++ b/docs/core-data-structures/session.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -session.md: d789ffcabb5cb0c744e265b61e322831c1d8a04f -session.zh.md: f4f102861db7403520e9f38cb56613e430718cbe +session.md: 201f366388b7a122ea0b59ce95b20b379f771796 +session.zh.md: b145334dec7483397a8cb6c41c50d3653d8f6972 diff --git a/docs/core-data-structures/tools.i18n.yaml b/docs/core-data-structures/tools.i18n.yaml index 56581614ec..ad44504663 100644 --- a/docs/core-data-structures/tools.i18n.yaml +++ b/docs/core-data-structures/tools.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -tools.md: 4a8081154a12a0d30e90f8b4df059ddc4257bba0 -tools.zh.md: 74f543548a54e532d1a858dee33bed906e82f4b3 +tools.md: 402fb49442db0501f138c65f23726c9eb1c23a88 +tools.zh.md: 2b0c078376d2ae971818f5ce5a19f352048fc11d diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index d9521f7d67..48572bf13c 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -7,37 +7,34 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:353`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`tui`](../packages/ui/tui) | -| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:350`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) | -| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:285`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:294`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:498`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:326`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent) | -| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:340`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent) | -| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:316`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:448`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy) | -| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:379`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) | -| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:395`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | -| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:409`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:463`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry), [`plan-mode`](../packages/plan/plan-mode) | -| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:424`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) | -| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:363`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:303`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:436`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | -| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:474`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode) | -| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:485`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tool-goal`](../packages/goal/tool-goal) | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:140`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`tui`](../packages/ui/tui) | +| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:294`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session) | +| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:236`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:245`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | +| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:403`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | +| `agent/idle` | `emit` | [`packages/core/agent/src/types.ts:390`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:272`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`tui`](../packages/ui/tui) | +| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:284`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`tui`](../packages/ui/tui) | +| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:262`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session) | +| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:322`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | +| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:348`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:363`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:307`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:254`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), `apiproxy`, [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | +| `agent/step` | `serial` | [`packages/core/agent/src/types.ts:335`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`plan-mode`](../packages/plan/plan-mode), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`time-context`](../packages/context/time-context), [`tool-skill`](../packages/skill/tool-skill), [`user-approval`](../packages/ui/user-approval), [`workspace-context`](../packages/context/workspace-context) | +| `agent/stopping` | `serial` | [`packages/core/agent/src/types.ts:378`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | | `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:30`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp) | | `commands/change` | `emit` | [`packages/ui/commands/src/index.ts:103`](../packages/ui/commands/src/index.ts) | [`commands`](../packages/ui/commands) (`events.dispatch`) | [`tui`](../packages/ui/tui) | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `apiproxy`, [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace) | | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:62`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:71`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) | | `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:54`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | -| `goal/changed` | `emit` | [`packages/goal/goal/src/types.ts:167`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) | +| `goal/changed` | `emit` | [`packages/goal/goal/src/types.ts:169`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-session`](../packages/goal/goal-session) | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:52`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/support/llm-replay), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`session-title`](../packages/session-title/session-title) | -| `session/created` | `emit` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`user-approval`](../packages/ui/user-approval) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:89`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title) | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:101`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`cli-demo`](../packages/examples/cli-demo), [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tui`](../packages/ui/tui), [`user-approval`](../packages/ui/user-approval), [`workspace`](../packages/workspace/workspace), [`workspace-context`](../packages/context/workspace-context) | -| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:111`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | +| `session/created` | `emit` | [`packages/core/session/src/index.ts:70`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`user-approval`](../packages/ui/user-approval) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:80`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:92`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`cli-demo`](../packages/examples/cli-demo), [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`jsonrpc`](../packages/ui/jsonrpc), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title), [`token-meter`](../packages/llm/token-meter), [`tui`](../packages/ui/tui), [`user-approval`](../packages/ui/user-approval), [`workspace`](../packages/workspace/workspace), [`workspace-context`](../packages/context/workspace-context) | +| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:102`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc), [`subagent`](../packages/subagent/subagent) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:113`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:119`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index cfa787d4e3..92fcdae160 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -78,7 +78,7 @@ export type SessionEvent = { }[T] ``` -Sources: [`packages/core/session/src/types.ts:324`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:337`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:366`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:398`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:269`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:282`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:311`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) ## Events @@ -150,7 +150,7 @@ Source: [`packages/ui/user-approval/src/index.ts:67`](../packages/ui/user-approv Types: [StreamChunk](core-data-structures/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:270`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:215`](../packages/core/session/src/types.ts) #### `assistant/message` — surface @@ -166,7 +166,7 @@ Source: [`packages/core/session/src/types.ts:270`](../packages/core/session/src/ Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:222`](../packages/core/session/src/types.ts) ### `compact/*` @@ -315,22 +315,6 @@ Source: [`packages/ui/permission/src/index.ts:36`](../packages/ui/permission/src Source: [`packages/plan/plan-mode/src/index.ts:40`](../packages/plan/plan-mode/src/index.ts) -### `prompt/*` - -#### `prompt/blocked` — log-only - -```ts persistence-catalog -/** - * Durable record of a prompt veto and its reason. It is log-only: the blocked - * prompt never enters the model-visible surface, and its turn runs zero steps. - */ -'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string } -``` - -Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md) - -Source: [`packages/core/session/src/types.ts:268`](../packages/core/session/src/types.ts) - ### `request/*` #### `request/header` — log-only @@ -343,7 +327,7 @@ Source: [`packages/core/session/src/types.ts:268`](../packages/core/session/src/ 'request/header': { header: EpochHeader; reason: RequestHeaderReason } ``` -Source: [`packages/core/session/src/types.ts:312`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:257`](../packages/core/session/src/types.ts) ### `sandbox/*` @@ -377,7 +361,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:34`](../packages/s Types: [SessionTitleEventData](core-data-structures/session-title.md) -Source: [`packages/session-title/session-title/src/index.ts:96`](../packages/session-title/session-title/src/index.ts) +Source: [`packages/session-title/session-title/src/index.ts:95`](../packages/session-title/session-title/src/index.ts) #### `session/title-llm-request` — log-only @@ -396,10 +380,10 @@ Source: [`packages/session-title/session-title-llm/src/index.ts:44`](../packages ```ts persistence-catalog /** Steering content injected between steps of a running turn. */ -'steering/message': PromptMessageData & { turn: number } +'steering/message': UserMessageData & { turn: number } ``` -Source: [`packages/core/session/src/types.ts:305`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:250`](../packages/core/session/src/types.ts) ### `step/*` @@ -410,7 +394,7 @@ Source: [`packages/core/session/src/types.ts:305`](../packages/core/session/src/ 'step/end': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:204`](../packages/core/session/src/types.ts) #### `step/start` — log-only @@ -419,7 +403,7 @@ Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/ 'step/start': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:202`](../packages/core/session/src/types.ts) ### `todo/*` @@ -432,7 +416,7 @@ Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/ Types: [TodoItem](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:307`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts) ### `tool/*` @@ -449,7 +433,7 @@ Source: [`packages/core/session/src/types.ts:307`](../packages/core/session/src/ Types: [CallId](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:228`](../packages/core/session/src/types.ts) #### `tool/code-dispatch` — log-only @@ -503,7 +487,7 @@ Source: [`packages/core/tools/src/code-mode.ts:34`](../packages/core/tools/src/c Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:295`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:240`](../packages/core/session/src/types.ts) ### `turn/*` @@ -521,23 +505,20 @@ Source: [`packages/core/session/src/types.ts:295`](../packages/core/session/src/ Types: [TurnEndReason](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:249`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:200`](../packages/core/session/src/types.ts) #### `turn/start` — log-only ```ts persistence-catalog /** - * Opens turn `turn`. `trigger` records what started it — one claimed queued - * message or an idle-time injection. The turn is the durability/replay - * boundary: every event sits between a `turn/start` and its matching - * `turn/end` (the turn-enclosure invariant). + * Opens turn `turn`. `trigger` records what started the model loop. */ 'turn/start': { turn: number; trigger: TurnTrigger } ``` Types: [TurnTrigger](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:242`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:193`](../packages/core/session/src/types.ts) ### `user/*` @@ -549,11 +530,10 @@ Source: [`packages/core/session/src/types.ts:242`](../packages/core/session/src/ * (the queued message claimed for this turn), a synthetic `agent.inject()` * context (file-change notices, subdir AGENTS.md, skill content, cron * notifications, …), or an admitted goal continuation round. All three - * project their `content` verbatim; `source` (with a non-`user` kind marking - * injected context) is the only channel that tells them apart. An idle - * injection wraps this event in a one-shot turn so the log stays turn-enclosed. + * project their `content` verbatim; `source` tells them apart. An idle + * injection may append this event between turns without running the model. */ -'user/message': PromptMessageData +'user/message': UserMessageData ``` -Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:213`](../packages/core/session/src/types.ts) diff --git a/examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl b/examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl index f164c7fe62..69b5a3033e 100644 --- a/examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl +++ b/examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl @@ -8,12 +8,14 @@ {"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"model returned a completed response with no content","code":"EMPTY_RESPONSE"}}}}} {"type":"step/end","seq":7,"time":0,"data":{"turn":1,"step":1}} {"type":"llm/retry","seq":8,"time":0,"data":{"turn":1,"step":1,"retry":1,"maxRetries":2,"delayMs":1,"failure":{"message":"model returned a completed response with no content","code":"EMPTY_RESPONSE"}}} -{"type":"step/start","seq":9,"time":0,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} -{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"Recovered."}}} -{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Recovered."}}}} -{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":3}}}} -{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":15,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"Recovered."}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":12,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} -{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":17,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"turn/end","seq":9,"time":1785047244285,"data":{"turn":1,"reason":{"kind":"error","step":1,"failure":{"message":"model returned a completed response with no content","code":"EMPTY_RESPONSE"}}}} +{"type":"turn/start","seq":10,"time":1785047244285,"data":{"turn":2,"trigger":{"kind":"retry"}}} +{"type":"step/start","seq":11,"time":1785047244289,"data":{"turn":2,"step":1}} +{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"Recovered."}}} +{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"Recovered."}}}} +{"type":"assistant/chunk","seq":15,"time":1785047244294,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":16,"time":1785047244294,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":17,"time":1785047244294,"data":{"turn":2,"step":1,"content":[{"type":"text","text":"Recovered."}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":12,"outputTokens":3}},"sourceEventSeqs":[12,13,14,15,16],"surfaceOp":"append"} +{"type":"step/end","seq":18,"time":1785047244294,"data":{"turn":2,"step":1}} +{"type":"turn/end","seq":19,"time":1785047244294,"data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/packages/acp/acp/src/index.ts b/packages/acp/acp/src/index.ts index 6982265fa8..7fc7c939e4 100644 --- a/packages/acp/acp/src/index.ts +++ b/packages/acp/acp/src/index.ts @@ -77,6 +77,12 @@ interface SessionRecord { resolve: (reason: StopReason) => void reject: (error: Error) => void turn: number | undefined + /** + * A failed turn's terminal reason, held until quiescence: a retry turn + * (`agent.retry()` closes the failed turn and opens a successor) adopts + * the prompt instead, so rejecting at `turn/end` would race the recovery. + */ + pendingError: Extract | undefined } | undefined } @@ -125,15 +131,11 @@ export function apply(ctx: Context, config: AcpConfig): void { inflight.resolve(reason) } - const settleFromTurnEnd = ( + const rejectFromError = ( inflight: NonNullable, - reason: TurnEndReason, + reason: Extract, ): void => { - if (reason.kind === 'error') { - inflight.reject(internalError(`turn failed: ${'failure' in reason ? reason.failure.message : reason.message}`)) - return - } - inflight.resolve(turnEndToStopReason(reason)) + inflight.reject(internalError(`turn failed: ${'failure' in reason ? reason.failure.message : reason.message}`)) } // Emit only committed assistant text. Raw chunks, reasoning, tools, plans, @@ -162,10 +164,22 @@ export function apply(ctx: Context, config: AcpConfig): void { if (inflight.turn === undefined && event.data.trigger.kind === 'message' && event.data.trigger.source.kind === 'user') { inflight.turn = event.data.turn + } else if (inflight.pendingError !== undefined && event.data.trigger.kind === 'retry') { + // A recovery policy opened a retry turn on the failed history: the + // prompt rides it instead of rejecting on the failed turn's end. + inflight.turn = event.data.turn + inflight.pendingError = undefined } } else if (inflight !== undefined && event.type === 'turn/end' && inflight.turn === event.data.turn) { - record.inflight = undefined - settleFromTurnEnd(inflight, event.data.reason) + if (event.data.reason.kind === 'error') { + // Hold the rejection: agent.retry() may adopt the prompt with a + // successor turn; quiescence without one delivers this error. + inflight.turn = undefined + inflight.pendingError = event.data.reason + } else { + record.inflight = undefined + inflight.resolve(turnEndToStopReason(event.data.reason)) + } } } }) @@ -255,7 +269,10 @@ export function apply(ctx: Context, config: AcpConfig): void { // turn cannot slip past correlation; a synchronous followup() // failure (invalid input) must free the slot again or the session // would reject every later prompt as already in flight. - record.inflight = { resolve, reject, turn: undefined } + const inflight: NonNullable = { + resolve, reject, turn: undefined, pendingError: undefined, + } + record.inflight = inflight try { record.agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } catch (error: unknown) { @@ -266,6 +283,19 @@ export function apply(ctx: Context, config: AcpConfig): void { const detail = error instanceof Error ? error.message : String(error) throw internalError(`prompt was not queued: ${detail}`) } + // Admission is pre-turn and retries outlive their failed turn, so a + // turnless slot settles only at quiescence: a held failure rejects + // (no retry adopted the prompt); no turn at all means admission + // discarded the prompt — report cancelled. + void record.agent.whenIdle().then(() => { + if (record.inflight !== inflight || inflight.turn !== undefined) return + record.inflight = undefined + if (inflight.pendingError !== undefined) { + rejectFromError(inflight, inflight.pendingError) + return + } + inflight.resolve('cancelled') + }) }) return { stopReason } },