Skip to content

🤖 fix: keep workspace turns alive across bash-monitor-wake queue cuts - #3797

Merged
ThomasK33 merged 3 commits into
mainfrom
fix/workspace-turn-settlement-monitor-wake
Aug 6, 2026
Merged

🤖 fix: keep workspace turns alive across bash-monitor-wake queue cuts#3797
ThomasK33 merged 3 commits into
mainfrom
fix/workspace-turn-settlement-monitor-wake

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

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:

  1. TaskService treated any correlated stream-end with finishReason !== "stop" as terminal, settling the workspace-turn handle as error on every queue cut.
  2. The continuation streams' last user message is the monitor wake (muxMetadata.type: "bash-monitor-wake"), so AgentSession dropped the workspace-turn correlation. The turn's real terminal stop finish was uncorrelated and could never settle or repair the handle — recovery scans only ever re-found the correlated tool-calls message 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

  • AgentSession: new inheritOpenWorkspaceTurnMetadata() — when the send is a bash-monitor-wake continuation, scan history newest→oldest; a correlated assistant message that ended with tool-calls (a queue cut) keeps the turn open and the continuation inherits its correlation. Any manual user input, uncorrelated assistant, correlated stop, or partial message closes the chain. Inherited metadata persists on each continuation's assistant message, so chains survive restarts.
  • TaskService: finalizeWorkspaceTurnFromStreamEnd now defers settlement (via the existing deferred-message machinery, handle stays running) for a correlated tool-calls stream-end while the child has a queued/preparing/streaming continuation. Genuine terminal tool-calls finishes with no continuation still settle as error. Races self-heal through the existing allowTerminalResettle/self-heal paths.

Validation

  • 11 new tests: 7 inheritance-scan behaviors + 2 end-to-end through real AgentSession.sendMessage (wake continuation inherits, manual message does not), plus 2 TaskService tests (tool-calls defers then completes via the continuation's correlated stop; 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-check green.
  • Root cause confirmed against the live session data of the affected workspaces (three handles erred with tool-calls while the child's history shows monitor wakes and a clean final stop).

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

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

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/agentSession.ts
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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/taskService.ts Outdated
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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 93e09bae77

ℹ️ 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".

@ThomasK33
ThomasK33 added this pull request to the merge queue Aug 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 6, 2026
@ThomasK33
ThomasK33 added this pull request to the merge queue Aug 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 6, 2026
@ThomasK33
ThomasK33 added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 78591b4 Aug 6, 2026
21 of 22 checks passed
@ThomasK33
ThomasK33 deleted the fix/workspace-turn-settlement-monitor-wake branch August 6, 2026 07:57
mux-bot Bot added a commit that referenced this pull request Aug 6, 2026
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.
@mux-bot mux-bot Bot mentioned this pull request Aug 6, 2026
mux-bot Bot added a commit that referenced this pull request Aug 6, 2026
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.
mux-bot Bot added a commit that referenced this pull request Aug 7, 2026
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.
mux-bot Bot added a commit that referenced this pull request Aug 7, 2026
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.
mux-bot Bot added a commit that referenced this pull request Aug 7, 2026
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.
mux-bot Bot added a commit that referenced this pull request Aug 8, 2026
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.
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