🤖 fix: keep workspace turns alive across bash-monitor-wake queue cuts - #3797
Conversation
A queued bash-monitor wake dispatched at a tool boundary cuts the child's in-flight stream with finishReason "tool-calls" while the child seamlessly continues the same delegated turn. TaskService treated any non-"stop" correlated stream-end as terminal, settling the workspace-turn handle with "Workspace turn ended before completion (finishReason: tool-calls)" even though nothing was interrupted — and the continuation streams carried no workspace-turn correlation, so the turn's real outcome could never repair the handle. The parent then falsely re-prompted the child about an interruption. Two-part fix: - AgentSession: bash-monitor-wake continuation streams inherit the open workspace-turn correlation (newest-first history scan that only crosses monitor wakes and correlated "tool-calls" queue cuts), so the continuation's terminal stream-end settles the handle with the real outcome. Persisted assistant metadata keeps chains alive across restarts. - TaskService: a correlated "tool-calls" stream-end with a queued/preparing/ streaming continuation in the child defers settlement (handle stays running) instead of erroring. Genuine terminal "tool-calls" finishes with no continuation still settle as errors.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ce7831e909
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex P2: when a bash-monitor-wake continuation triggers on-send auto-compaction, the correlated queue-cut assistant lands behind the new compaction boundary, so the wake continuation's history scan found nothing and the delegated turn's real outcome could never settle the handle. - Capture the open turn correlation from pre-compaction history at the on-send divert and stamp it on the compaction follow-up (CompactionFollowUpRequest.workspaceTurnMetadata); the stamp persists on the summary's pendingFollowUp, so the continuation re-inherits it via inheritOpenWorkspaceTurnMetadata (crash-safe across restarts). - Ignore compact-agent stream-ends in the uncorrelated workspace-turn interrupt path: a compaction turn is a mechanical context operation and must not supersede a still-running delegated turn.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b91640301
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex P2: deferring on any queued/preparing turn treated superseding input (manual message, bare /compact) as proof the delegated turn was continuing, which could leave the handle stuck running with only a deferred pre-boundary message until restart. The deferral now requires wake-specific evidence of the same turn: - AgentSession.hasPendingBashMonitorWakeContinuation: the next queued entry is a bash-monitor wake, or a dequeued wake is mid-dispatch (tracked via dispatchingQueuedEntryMuxMetadata across the dequeue->stream-start window, avoiding stale activeStreamContext races). - Otherwise, TaskService matches the active stream's inherited correlation (getStreamInfo now exposes muxMetadata; an active stream is necessarily newer than the ended one since streams leave STARTING/STREAMING before their stream-end is emitted). Superseding queued input settles the handle immediately again (error), and a streaming inherited continuation defers. Two new tests cover both branches.
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Both call sites added in #3797 re-implemented the same unknown -> wake check with different unsound casts. Extract isBashMonitorWakeMetadata alongside the sibling guards in messageQueue.ts and reuse it, so the queue-head and mid-dispatch checks cannot drift.
Both call sites added in #3797 re-implemented the same unknown -> wake check with different unsound casts. Extract isBashMonitorWakeMetadata alongside the sibling guards in messageQueue.ts and reuse it, so the queue-head and mid-dispatch checks cannot drift.
Both call sites added in #3797 re-implemented the same unknown -> wake check with different unsound casts. Extract isBashMonitorWakeMetadata alongside the sibling guards in messageQueue.ts and reuse it, so the queue-head and mid-dispatch checks cannot drift.
Both call sites added in #3797 re-implemented the same unknown -> wake check with different unsound casts. Extract isBashMonitorWakeMetadata alongside the sibling guards in messageQueue.ts and reuse it, so the queue-head and mid-dispatch checks cannot drift.
Both call sites added in #3797 re-implemented the same unknown -> wake check with different unsound casts. Extract isBashMonitorWakeMetadata alongside the sibling guards in messageQueue.ts and reuse it, so the queue-head and mid-dispatch checks cannot drift.
Both call sites added in #3797 re-implemented the same unknown -> wake check with different unsound casts. Extract isBashMonitorWakeMetadata alongside the sibling guards in messageQueue.ts and reuse it, so the queue-head and mid-dispatch checks cannot drift.
Summary
Fixes false "Workspace turn ended before completion (finishReason: tool-calls)" failures on delegated workspace turns whose child workspaces use background bash monitors. The owning parent no longer sees a spurious error and re-prompts the child about an interruption that never happened.
Background
A queued bash-monitor wake dispatched at a tool boundary intentionally cuts the child's in-flight stream (
finishReason: "tool-calls") and immediately continues the same delegated work in a new stream. Two bugs turned this benign cut into a terminal failure:TaskServicetreated any correlated stream-end withfinishReason !== "stop"as terminal, settling the workspace-turn handle aserroron every queue cut.muxMetadata.type: "bash-monitor-wake"), soAgentSessiondropped the workspace-turn correlation. The turn's real terminalstopfinish was uncorrelated and could never settle or repair the handle — recovery scans only ever re-found the correlatedtool-callsmessage and re-erred.Observed live: a parent delegating a fresh-context PR review re-prompted its child three times ("your previous turn ended prematurely") while the child was progressing normally through monitor wakes.
Implementation
inheritOpenWorkspaceTurnMetadata()— when the send is a bash-monitor-wake continuation, scan history newest→oldest; a correlated assistant message that ended withtool-calls(a queue cut) keeps the turn open and the continuation inherits its correlation. Any manual user input, uncorrelated assistant, correlatedstop, or partial message closes the chain. Inherited metadata persists on each continuation's assistant message, so chains survive restarts.finalizeWorkspaceTurnFromStreamEndnow defers settlement (via the existing deferred-message machinery, handle staysrunning) for a correlatedtool-callsstream-end while the child has a queued/preparing/streaming continuation. Genuine terminaltool-callsfinishes with no continuation still settle aserror. Races self-heal through the existingallowTerminalResettle/self-heal paths.Validation
AgentSession.sendMessage(wake continuation inherits, manual message does not), plus 2 TaskService tests (tool-calls defers then completes via the continuation's correlatedstop; tool-calls without continuation still errors).taskService.test.ts: 344 pass; related agentSession suites (queueDispatch, startupAutoRetry, waitForIdle, new inheritance file): 48+9 pass;make static-checkgreen.tool-callswhile the child's history shows monitor wakes and a clean finalstop).Risks
Touches workspace-turn settlement, which parents rely on for delegated-work outcomes. The deferral is narrowly gated (correlated event +
finishReason === "tool-calls"+ live continuation evidence), and metadata inheritance is restricted to bash-monitor-wake sends, so ordinary turns, manual prompts, and other synthetic messages are unaffected. Worst case for a mis-deferral is the existing deferred-recovery path settling the handle from history rather than an immediate error.Generated with
mux• Model:anthropic:claude-fable-5• Thinking:off• Cost:$22.19