🤖 fix: wake stream-ends no longer falsely settle active workspace turns - #3949
🤖 fix: wake stream-ends no longer falsely settle active workspace turns#3949coadler wants to merge 4 commits into
Conversation
Sub-agent progress reports, bash-monitor wakes, and family messages dispatch synthetic user turns inside a child workspace. Their streams carry no workspace-turn correlation metadata, so their stream-end hit the uncorrelated-supersede path and settled the still-running delegated turn as interrupted. The owner saw a false terminal while the child kept working. The supersede interrupt now scans child history between the turn prompt and the uncorrelated stream-end. Only manual (non-synthetic) user input in that window proves the workspace was redirected; synthetic wake continuations keep the handle active so the real terminal report can settle it later.
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65cec367c1
ℹ️ 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 review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 65cec367c1
ℹ️ 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".
This comment has been minimized.
This comment has been minimized.
…d turns Codex review found two gaps in the uncorrelated wake guard: - An unconditional ignore could strand the handle as running when the synthetic wake stream was the delegated turn's last activity. The guard now defers only while continuation evidence is live (queued input, auto-retry, monitor wakes, correlated continuations, another active stream) and otherwise settles the handle from the wake event itself. A later correlated end still corrects it via allowTerminalResettle. - After auto-compaction, getHistoryFromLatestBoundary no longer shows the correlated prompt, so promptIndex stayed -1 and post-compaction wakes fell through to the supersede path. The history scan now also anchors on compaction-summary.pendingFollowUp.workspaceTurnMetadata, mirroring AgentSession.inheritOpenWorkspaceTurnMetadata.
|
@codex review Addressed both P1 threads (PRRT_kwDOPxxmWM6bz40N, PRRT_kwDOPxxmWM6bz40Q) in 2314bfe:
|
This comment has been minimized.
This comment has been minimized.
Second Codex round: returning early on getHistoryFromLatestBoundary failure left the active handle running, and for a normal created workspace the owner's waiter then never settles because this stream-end was the only settlement signal. Restore the pre-existing supersede settlement as the terminal fallback and pin it with a test that injects the failure for every history read.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2314bfedb6
ℹ️ 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 Review: Didn't find any major issues. Hooray! 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". |
This comment has been minimized.
This comment has been minimized.
Third Codex round, three gaps: - Nested-work blindness: continuation evidence now includes hasActiveWorkspaceTurnDeferredBlockers (descendant tasks, workflows, nested turns), which a continuing reported-agent workspace skips on the parent side of handleStreamEnd. - Immutable completion: uncorrelated wake settlements are stamped provisionalOutcome; settleWorkspaceTurn lets a later strictly- correlated stream-end replace them even when completed. - Vanished continuations: deferral persists the wake end as a deferred message ID (new persistDeferredUncorrelatedWakeEnd, correlation-free sibling of markWorkspaceTurnStreamEndDeferred). Stale recovery reads deferred uncorrelated ends back via buildDeferredWakeEndEventFromHistory, so a continuation that dies without another stream-end settles deterministically. isLiveWorkspaceTurn now counts auto-retry and monitor-wake continuations as runtime activity.
|
@codex review Addressed all three round-3 threads in 69541ae:
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69541ae996
ℹ️ 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".
| const event = | ||
| this.buildWorkspaceTurnStreamEndEventFromHistory(record, message) ?? | ||
| (isDeferred ? this.buildDeferredWakeEndEventFromHistory(record, message) : null); |
There was a problem hiding this comment.
Mark recovered uncorrelated wake outcomes as provisional
When blockers disappear, a deferred uncorrelated wake is rebuilt here through buildTerminalWorkspaceTurnRecordFromEvent, which explicitly deletes provisionalOutcome. If snapshot/stale recovery wins a race with a correlated stream-end callback, the recovered completed record is therefore immutable and the later real terminal is ignored. Preserve the provisional marker for this deferred uncorrelated path just as the immediate wake-settlement path does.
Useful? React with 👍 / 👎.
| .passthrough() | ||
| .optional(), | ||
| deferredMessageIds: z.array(z.string().min(1)).optional(), | ||
| provisionalOutcome: z.boolean().optional(), |
There was a problem hiding this comment.
Keep provisional handle records readable after downgrade
After this version writes provisionalOutcome, downgrading to the parent version makes its .strict() handle schema reject the unknown persisted key; readWorkspaceTurnFile then returns null, so task_await reports the handle as missing and startup reconciliation skips it. Store this state in a backward-readable representation or otherwise avoid adding an unknown top-level field to these persistent records.
AGENTS.md reference: AGENTS.md:L10-L11
Useful? React with 👍 / 👎.
| await this.taskHandleStore.upsertWorkspaceTurn({ | ||
| ...current, | ||
| updatedAt: getIsoNow(), | ||
| deferredMessageIds: [...(current.deferredMessageIds ?? []), event.messageId], | ||
| }); |
There was a problem hiding this comment.
Preserve deferred wake outcomes across history boundaries
Persisting only the message ID is insufficient when the continuation performs compaction and then fails to dispatch: the deferred wake precedes the new durable boundary, so getHistoryFromLatestBoundary() can no longer return that message. Once the continuation signals clear, recovery finds no terminal event, and settleStaleWorkspaceTurn deliberately keeps any same-process record with deferredMessageIds active, leaving the owner and parallel-task slot blocked indefinitely until restart. Persist the deferred event's terminal data on the handle or search older history epochs during recovery.
Useful? React with 👍 / 👎.
| function isManualChildWorkspaceInput(message: MuxMessage): boolean { | ||
| return message.role === "user" && message.metadata?.synthetic !== true; |
There was a problem hiding this comment.
Treat user-triggered auto-compaction as manual supersession
When an uncorrelated wake end is processed after on-send auto-compaction for a newly queued user prompt has been persisted, that compaction request is marked synthetic: true, so this predicate does not recognize the redirect. If the compaction and queue are no longer reported as live, the old handle is completed from the wake output instead of interrupted as superseded. The existing isManualUserSupersessionMessage already distinguishes these user-triggered auto-compactions from internal-resume compactions; reuse that predicate rather than introducing a less complete duplicate.
AGENTS.md reference: AGENTS.md:L200-L202
Useful? React with 👍 / 👎.
Summary
A sub-agent report, bash-monitor wake, or family message can end its stream inside a child workspace while a delegated workspace turn still runs. That stream-end carries no turn correlation metadata. The uncorrelated-supersede path then settled the active handle as
interruptedwith "Workspace turn superseded by an uncorrelated workspace stream-end". The owner saw a false terminal. The child kept working and later pushed commits and opened PRs that no wake-up ever reported.Background
Three production instances (workspaces
441bbc205d,1fb57a782d,0b726f0717) showed the same shape. Each child spawned explore sub-agents or armed bash monitors. Timing matched wake delivery: one false terminal arrived minutes after spawn, before any push.The existing guards only covered two cases: compaction streams (
agentId === "compact") and uncorrelated ends that predate the turn prompt in history. Mid-turn synthetic wake streams fell through to the interrupt path.Implementation
interruptWorkspaceTurnFromUncorrelatedStreamEndnow scans child history between the turn anchor and the uncorrelated stream-end, with three behaviors:metadata.synthetic !== true) in the window proves the workspace was redirected away from the delegated work. The handle settlesinterrupted, as before.allowTerminalResettle.Two additional fixes from review:
compaction-summary.pendingFollowUp.workspaceTurnMetadata(mirroringAgentSession.inheritOpenWorkspaceTurnMetadata), so post-compaction wakes stay guarded.isStreamEndBeforeWorkspaceTurnPrompthelper merged into the handler because both checks share one history scan.Validation
status: "interrupted"with the supersede error string, mid-work, while the child keeps streaming.taskService.test.ts: 410 pass. Adjacent suites:agentSession.workspaceTurnInheritance,agentSession.queueDispatch,acp.promptCorrelationall pass.make typecheckandmake lintpass.Risks
Low-to-moderate. The change narrows the false-interrupt path but adds a new settlement route (idle wake ends settle completed). Two mitigations bound the risk: settlement only fires when no continuation evidence is live anywhere in the child, and
allowTerminalResettle: truelets any later correlated end correct a premature completion. Residual worst case is a report whose text comes from a wake response rather than the turn's true final message — degraded fidelity, not lost work.Generated with
xum• Model:openrouter:stealth/ox-alpha• Thinking:high• Cost:$0.01