Skip to content

🤖 fix: defer bash-monitor wakes until the active turn ends - #4114

Draft
ibetitsmike wants to merge 16 commits into
mainfrom
mike/bash-wake-turn-end
Draft

🤖 fix: defer bash-monitor wakes until the active turn ends#4114
ibetitsmike wants to merge 16 commits into
mainfrom
mike/bash-wake-turn-end

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Bash-monitor attention no longer cuts an active turn. While the owner workspace is busy (a queued or preparing turn, or a session-backed stream), a monitor match is deferred through the existing reconcile-after-idle path instead of being queued as a tool-end message, and the wake itself is sent with requireIdle. The turn finishes in its original stream and the agent answers on the same tool result; the owed attention arrives as a separate wake once the workspace is idle, or is withdrawn if the agent already consumed that output. A hard Stop retires the attention that is currently owed while leaving the monitor armed. Two independent fixes found along the way ride along: queue correlation readers skip withdrawn entries, and aborted-stream usage is priced against the effective (fallback) model.

Supersedes #4065, which is left open for its author to close.

Background

#4065 addressed this incident: monitored background bash tasks settled while the agent was streaming, the wake was queued as a tool-end message, the stream was stopped at the next tool boundary (finishReason: "tool-calls"), the agent's own task_await had already consumed the output so the reconciler withdrew the wake, and the turn was left stranded on an unanswered tool result until a human typed. #4065 kept the mid-turn cut and added detection of stranded turns plus a synthetic [CONTINUE] restart, a continuation lifecycle, a recovery loop and caps (+1,563 net lines across 22 files).

This PR removes the cause instead: attention that the running turn can consume must never be allowed to cut that turn. Production diff here is +428 / -162 lines; the rest is tests.

Implementation

  • WorkspaceService.dispatchBashMonitorWake: when the owner has a pending or preparing turn or a session-backed busy state, schedule scheduleBashMonitorWakeReconcileAfterIdle and return "deferred"; otherwise send the wake with requireIdle: true (no queueDispatchMode: "tool-end", no queue dedupe key, no abort listener that removed queued entries). A wake that loses the race with a user send is skipped by the existing requireIdle preflight and re-reconciled after idle, so manual input is never held behind background attention.
  • WorkspaceService.interruptStream with the new retireBashMonitorAttention option (passed by the user Stop entry points: the Stop button, the Escape keybind, the command palette Stop and ACP cancel; internal interrupts such as goal promotion, archive, ACP disconnect and send-now leave monitor output owed): before the interrupt, BashMonitorWakeReconciler.consumeCurrent withdraws any in-flight wake dispatch and marks the currently owed attention consumed, so the abort's own idle transition has nothing to send and no wake fires for output the user just stopped around. It runs before the abort because session.interruptStream returns only after the abort settled, when an idle-triggered dispatch may already be admitting; it does not take bashMonitorHistoryLocks, so Stop never waits behind a wake admission that holds the lock across preflight and stream construction. Retirement is bounded to the frontier snapshotted when the Stop was requested: output, settlements, monitor failures and newly armed monitors that land while the Stop settles stay owed and wake normally once idle. When the retirement or the abandon marker below cannot be written, the stream is still aborted but interruptStream returns Err(STOP_UNRECORDED_MESSAGE); the reconciler keeps the retirement owed and retries it before any later wake and on the next Stop. Every renderer Stop goes through stopStream(), which publishes that error to a workspace-keyed chat error store (chatErrorToasts.ts, replacing the one-shot child-budget toast event) that the workspace's chat input drains when it mounts, so the warning survives a workspace switch mid-Stop. The ACP cancel settles the pending prompt as cancelled for that sentinel too (the stream did stop) before reporting the durability failure; STOP_UNRECORDED_MESSAGE lives in common/constants/workspace.ts for that. Monitors stay registered and later output still wakes. consumeCurrent becomes public; the now-unread dedupeKey field is dropped from BashMonitorWakeDispatch.
  • AgentSession.sendMessage: a cancelable wake whose signal fires after the acceptance point of no return (its row is already durable) but before PREPARING now resolves Ok without starting a stream, the same contract as cancelBeforeAcceptance and the disposed path. Without this, a Stop issued while the wake was in goal sync saw no turn to abort and the wake started a stream after the Stop returned. The withdrawn wake records a startup auto-retry abandon marker so crash recovery does not replay its durable row; WorkspaceService.interruptStream joins the in-flight wake send and, if the marker write failed, retries it (recordPendingStartupAutoRetryAbandon) before reporting the Stop recorded.
  • MessageQueue: hasAllWorkspaceTurnContinuations, hasAllWorkspaceTurnContinuationsAheadOfPromotedToolEnd, hasNextWorkspaceTurnContinuation, getNextQueueCutCandidate and isNextEntryBashMonitorWake now read the first entry whose cancel signal has not fired (the rule getNextDispatchableMode already used), so a withdrawn entry can neither supersede nor misattribute a delegated turn's correlation.
  • StreamManager.cleanupAbortedStream adds model: streamInfo.model and the request-pinned metadataModel to the stream-abort metadata (schema gains both as optional), and AgentSession.handleTurnAbort prefers the effective model over the requested model string and passes metadataModel into goal accounting, mirroring the stream-end path. Usage of an aborted stream that fell back to another model is priced against the model that actually ran, and a Coder runtime ID keeps its pinned pricing identity instead of recording $0.

Validation

  • Producer-to-stream tests in workspaceService.test.ts drive a fake SDK stream through AIService.streamMessage and assert: repeated owed wakes during a turn never cut it and the answer arrives in the original stream; unconsumed attention coalesces into one wake after natural completion while idle attention starts promptly; a hard Stop retires owed attention without disarming later idle wakes, completes while another holder owns bashMonitorHistoryLocks, and returns STOP_UNRECORDED_MESSAGE when retirement I/O fails while the retirement still lands before any later wake; settlements, monitor failures and monitor arms that happen after the Stop request stay owed; stopStream retains an Err Stop for the workspace's chat input (shown on mount, after unmount, and not for another workspace) and stays silent on Ok; an unrecorded Stop still settles the ACP prompt as cancelled while cancel reports the failure; an interrupt without retireBashMonitorAttention keeps the attention owed; a Stop issued during a wake's acceptance window leaves the session idle with no stream and a later match still wakes; owed attention neither holds a delegated completion open nor inherits its closed correlation; a withdrawn idle wake rolls back its admission and a fresh delivery succeeds.
  • Red-green for the effective-model fix: removing the metadata.model fallback or the metadataModel pass-through fails the new streamManager and agentSession.queueDispatch cases (the Coder-ID case asserts a non-zero cost). Red-green for the Stop ordering: reinstating the lock-around-interrupt version times out the lock-holder test.
  • Remote UAT (Coder Agents on dogfood, claude-sonnet-5 through the gateway, xum built at c91286c and driven through the UI): PASS on all 10 scenarios. Three runs of a monitored task consumed in-turn by task_await ended in the original stream with no wake; unconsumed attention arrived as exactly one wake ~60 ms after the turn ended; an idle match woke promptly; a user message typed during a turn dispatched ahead of the wake and the wake followed that turn; Stop produced no wake for the stopped output while the next match woke normally; 10 rapid matches coalesced into one wake; reload kept the history intact. 7 wake rows total, all accounted for; no finishReason: "tool-calls" turn was left unanswered except the E1 turn the tester interrupted by hand, where the queued user message backgrounded the running foreground bash and dispatched at the tool boundary (existing behavior on main, not touched here).

Risks

Moderate, scoped to bash-monitor wakes and turn correlation.

  • Behavior change: a monitor match during a long turn is reported after the turn ends instead of at the next tool boundary. An agent that needs the output mid-turn still gets it through task_await; it is only the unsolicited wake that moves later.
  • The hard-Stop path retires owed attention before the abort rather than after it. Output that arrives during the few milliseconds the abort takes to settle is treated as post-Stop output and wakes normally once idle. session.interruptStream returns Ok even with no active stream, so pressing Stop on an idle workspace retires its owed attention as well; that matches the previous head. Only callers that pass retireBashMonitorAttention retire attention, so a caller added later without the option keeps the pre-PR behavior (output stays owed). A Stop whose retirement or abandon marker cannot be written reports an error (shown as a toast) instead of succeeding silently; the stream is still stopped and nothing is lost, the dismissed output may wake once on the next launch.
  • The queue reader change only affects entries whose cancel signal has already fired; those entries were already dispatched as no-ops.

Pains

The branch was rebased across #4109 (turn lifecycle centralization), which required porting the test harnesses from direct aiEmitter events to settling TurnStreamHandle.completion. Locally, bun on PATH resolved to 1.2.15 while the repo pins 1.3.5; under 1.2.15 the injected-failure tests in streamManager.test.ts and agentSession.queueDispatch.test.ts fail identically on main, which cost a diagnosis round.


Generated with xum • Model: anthropic:claude-fable-5-1 • Thinking: xhigh • Cost: $753.56

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@chatgpt-codex-connector

This comment has been minimized.

@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: c91286c4da

ℹ️ 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/streamManager.ts
Comment thread src/node/services/workspaceService.ts Outdated
Comment thread src/node/services/workspaceService.ts Outdated
Codex review on c91286c:
- hard Stop no longer takes bashMonitorHistoryLocks (a wake admission holds it
  across preflight and stream construction); consumeCurrent runs before the
  abort and is best-effort so a persistence failure cannot fail the Stop
- stream-abort metadata carries the request-pinned metadataModel so aborted
  Coder-runtime streams keep their pricing identity in goal accounting
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Addressed all three findings in 27acae7: abort metadata carries metadataModel for pinned pricing, hard Stop retires owed attention best-effort before the abort, and no longer takes bashMonitorHistoryLocks.

@chatgpt-codex-connector

This comment has been minimized.

@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: 27acae7dd7

ℹ️ 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/workspaceService.ts Outdated
Comment thread src/node/services/workspaceService.ts Outdated
…ly on user Stop

A cancelable monitor wake withdrawn past the acceptance point of no return (a hard
Stop retiring owed attention while the wake is in goal sync or acceptance) keeps its
durable rows but resolves Ok without starting a stream instead of claiming PREPARING
after the Stop returned.

interruptStream retires owed attention only when the caller passes
retireBashMonitorAttention (user Stop button, Escape, command palette, ACP cancel);
goal promotion, archive, ACP disconnect and send-now keep monitor output owed.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round 3 (12dd020): a wake withdrawn past the acceptance point of no return now resolves without claiming PREPARING, and interruptStream retires owed bash-monitor attention only when the caller passes retireBashMonitorAttention (user Stop entry points), not on every non-soft interrupt.

@chatgpt-codex-connector

This comment has been minimized.

@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: 12dd020f18

ℹ️ 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/browser/features/Messages/ChatBarrier/StreamingBarrier.tsx Outdated
Comment thread src/node/services/workspaceService.ts Outdated
Comment thread src/node/services/agentSession.ts
Comment thread src/node/services/workspaceService.ts
…onously; defer during pending compaction

- Compaction Stop paths (cancelCompaction, StreamingBarrier fallback) pass
  retireBashMonitorAttention so a match during compaction does not wake the agent.
- consumeCurrent aborts the in-flight dispatch before taking the owner lock;
  interruptStream starts retirement before the abort and awaits it after.
- Point-of-no-return withdrawal persists the user-abort abandon marker so startup
  recovery does not replay the retired wake.
- Wake dispatch treats a pending mid-stream compaction as turn work; the idle waiter
  waits on a deterministic settle signal from AgentSession.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round 4 (a8f06c1). Addressed the four round 3 findings:

  • Compaction Stop paths (cancelCompaction, StreamingBarrier fallback) pass retireBashMonitorAttention so a monitor match during compaction does not wake the agent once the session goes idle.
  • consumeCurrent aborts the in-flight dispatch synchronously before taking the owner lock; interruptStream starts retirement before session.interruptStream and awaits it afterwards.
  • The point-of-no-return withdrawal persists the user-abort abandon marker so startup recovery does not replay the retired wake.
  • Wake dispatch treats a pending mid-stream compaction as turn work (hasActiveOrPendingTurnWork); the idle waiter waits on a deterministic waitForMidStreamCompactionSettled signal.

Each fix has a red-green test; the ACP prompt-correlation expectation is updated for the retireBashMonitorAttention option.

@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: a8f06c1964

ℹ️ 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 Outdated
Comment thread src/node/services/workspaceService.ts Outdated

@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 Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: a8f06c1964

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/node/services/workspaceService.ts
…mmit retirement only after a successful stop

- The prefix-swap-invalidated path no longer settles the mid-stream compaction window
  before finishContinuousCompaction dispatches the saved continuation; the observation's
  finally settles it afterwards, as the usage-delta path already does.
- consumeCurrent takes a commit gate: it still withdraws the in-flight wake and reserves
  the reconciler lock before the abort, but advances watermarks only once the stop
  succeeded. A failed stop leaves the signals owed and schedules a reconcile.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round 5 (head 1f3cc0b). Addresses both Round 4 findings:

  • The prefix-swap-invalidated path no longer settles the mid-stream compaction window before finishContinuousCompaction dispatches the continuation; the observation's finally settles it afterwards, so idle monitor wakes cannot race the follow-up send for turn admission.
  • consumeCurrent takes a commit gate: it still withdraws the in-flight wake synchronously, but advances watermarks and cleans up registry rows only after session.interruptStream succeeded. A failed Stop leaves the signals owed and schedules a reconcile.

@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: 1f3cc0be6d

ℹ️ 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/bashMonitorWakeReconciler.ts Outdated
@chatgpt-codex-connector

This comment has been minimized.

A wake withdrawn past its point of no return (durable row, not yet PREPARING)
resolves only after recording the startup abandon marker for that row. Stop now
waits for that send to settle, so a forced exit right after Stop cannot leave the
row eligible for startup replay.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Head is now 4876b42 (supersedes the round 5 request on 1f3cc0b). Adds the fix for the security finding: interruptStream joins the withdrawn wake's send before acknowledging Stop, so the startup abandon marker for its durable row is written before Stop returns.

@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: 4876b427e7

ℹ️ 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/workspaceService.ts
consumeCurrent snapshots the outstanding signals before waiting on the stop
gate, so output that arrives while the stop settles is new and stays owed to
the idle agent instead of being consumed by the successful Stop.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Head is now 47f3759. Adds the fix for the snapshot finding: consumeCurrent collects the owed signals before waiting on the stop gate, so output arriving while Stop settles stays owed to the idle agent.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 47f37591cf

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

@chatgpt-codex-connector

This comment has been minimized.

… of no return

A hard Stop that withdraws a bash monitor wake after its row is durable but
while goal sync is pending left the row eligible for startup replay when goal
sync threw: AgentSession.sendMessage finalized onAccepted and rethrew before
reaching the abort-marker write, WorkspaceService.sendMessage resolved Err, and
interruptStream discarded the joined Result. Route the goal-sync catch, the
disposed exit, the onAccepted failure exit, and the existing withdrawn exit
through one abandonWithdrawnSend helper so the marker lands before the send
settles on every path, which is what the Stop join relies on.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round 6: 72038d8 records the startup abandon marker on every withdrawn-wake exit past the point of no return (goal-sync failure, disposed, onAccepted failure), so the Stop join always observes a recorded marker.

@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: 72038d802c

ℹ️ 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 Outdated
… point of no return

The goal-sync failure and disposed exits checked the cancel signal before
awaiting onAccepted, so a Stop that aborted the signal while the reconciler
was persisting acceptance was missed: the catch rethrew without an abandon
marker and Stop joined the send and returned success with the durable wake
row still eligible for startup replay. Run abandonWithdrawnSend in a finally
after onAccepted on those exits and after the accepted admission-stale
failure, so the check is the last await of every exit that skips PREPARING.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round 7: 80e6716 makes the withdrawal check the last await of every exit past the point of no return (onAccepted wrapped in try/finally on the goal-sync failure and disposed exits, plus the accepted admission-stale exit), so a Stop landing during acceptance I/O still records the abandon marker.

@chatgpt-codex-connector

This comment has been minimized.

@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: 80e6716d9e

ℹ️ 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
persistAutoRetryState swallowed write errors, so a wake withdrawn past its
point of no return resolved Ok even when the startup abandon marker never
reached disk, and Stop acknowledged success while leaving the trailing
synthetic row eligible for startup replay. The marker writer now reports
whether the file reflects memory; a withdrawn wake whose marker could not
be recorded fails with WITHDRAWN_WAKE_UNRECORDED_MESSAGE, and the joining
interruptStream propagates that failure. Other preference writes stay
best-effort.
@ibetitsmike

Copy link
Copy Markdown
Contributor 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: 8c9f5f68d3

ℹ️ 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/workspaceService.ts
Comment thread src/node/services/workspaceService.ts
Comment thread src/node/services/bashMonitorWakeReconciler.ts Outdated
Comment thread src/node/services/workspaceService.ts
Comment thread src/browser/hooks/useAIViewKeybinds.ts Outdated
…wake markers

Round 8 review fixes for bash monitor wakes:
- consumeCurrent snapshots the process frontier synchronously on entry and
  keeps it as an owed retirement in reconciler state once the stop commits;
  reconcileOnce retries it before any dispatch, so retirement I/O that fails
  cannot let the stop's idle reconcile re-dispatch dismissed attention, and
  output arriving while the stop waits behind acceptance stays owed
- the unrecorded startup abandon marker is tracked in AgentSession and
  retried on every later user Stop, which fails with STOP_UNRECORDED_MESSAGE
  until the marker is on disk
- the idle waiter releases its map slot before scheduling reconciliation so a
  reconcile that loses the race to a new turn installs the next idle wait
- the RetryBarrier Stop button issues the same attention-retiring interrupt
  as its keyboard shortcut
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

This comment has been minimized.

@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

...(snapshot.terminal == null && record?.terminal != null
? { terminal: record.terminal }
: {}),
...(record?.lost != null ? { lost: record.lost } : {}),

P2 Badge Preserve terminal signals created after Stop

When a monitored process settles while consumeCurrent() is waiting for the reconciler lock or stop commit, the captured live snapshot has no terminal state, but candidates() merges the later registry terminal/lost fields into that snapshot. retireOwed() consequently advances their watermarks and cleans them up, silently dismissing output that became owed after the Stop request instead of waking the now-idle agent. Keep registry state bounded to the same entry-time frontier as the live snapshot.

ℹ️ 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/bashMonitorWakeReconciler.ts
…me frontier

A user Stop whose monitor-attention retirement fails on I/O now returns
STOP_UNRECORDED_MESSAGE: the owed frontier survives only in memory, so a restart
before the retry could wake the agent on the dismissed output. The stream still
stops and the reconciler and the next Stop keep retrying the retirement.

Retirement against a stop-time frontier no longer merges registry terminal or
lost records the process manager produced after the snapshot, and no longer
treats a monitor armed since as a dead registry row. The live set is pulled after
the registry read on every path so a monitor armed mid-read cannot be mistaken
for a dead row either.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

Re the review-body finding "Preserve terminal signals created after Stop" (bashMonitorWakeReconciler.ts, candidates): fixed in 0508fd5. When candidates runs against a stop-time frontier it no longer merges registry terminal into a snapshot that had none (the snapshot's own terminal is authoritative: settlementDisposition is set in memory before the registry write), merges registry lost only into a snapshot that was already retired at the stop (a failure sets stopped synchronously before the event that records lost), and no longer treats a registry row for a monitor armed after the snapshot as a dead row (that path would have marked it lost and removed its row). The live set is now pulled after the registry read on every path too, so a monitor armed during the read cannot be taken for a dead row.

Tests (bashMonitorWakeReconciler.test.ts): consumeCurrent leaves a settlement recorded after the stop request owed, consumeCurrent leaves a monitor failure recorded after the stop request owed, consumeCurrent keeps the registry row of a monitor armed after the stop request. Each was red against the previous merge/filter and green with the fix.

@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round 9 fixes are in 0508fd5: a Stop whose monitor-attention retirement is not durable now fails with STOP_UNRECORDED_MESSAGE (retirement still retried by the reconciler and the next Stop), and retirement against a stop-time frontier no longer absorbs terminal/lost records or monitor arms that happened after the Stop request.

@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: 0508fd5c76

ℹ️ 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/workspaceService.ts

@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 Security Review

Here are some automated security review suggestions for this pull request.

Reviewed commit: 0508fd5c76

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/browser/features/Messages/ChatBarrier/RetryBarrier.tsx Outdated
Renderer Stop paths discarded the interruptStream Result, so a Stop the backend
could not record on disk looked successful. Route every user Stop through
stopStream(), which shows the backend's error in the workspace's chat input via
the (generalized) CHAT_ERROR_TOAST event.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round 10 fix is in 573bc3f: every renderer Stop path now goes through stopStream(), which awaits the interruptStream Result and surfaces STOP_UNRECORDED_MESSAGE as a chat error toast for that workspace (via the generalized CHAT_ERROR_TOAST event) instead of discarding it.

@chatgpt-codex-connector

This comment has been minimized.

@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: 573bc3f9dd

ℹ️ 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/browser/utils/stopStream.ts Outdated
Comment thread src/node/acp/agent.ts
…orded Stop

The Stop error toast was a one-shot window event filtered by workspaceId in
ChatInput, so a Stop that settled after the user switched workspaces lost its
STOP_UNRECORDED_MESSAGE warning. Chat errors now go through a workspace-keyed
store (publishChatError / useChatErrorToasts) that drains when that
workspace's input subscribes; the CHAT_ERROR_TOAST event is gone.

ACP cancel treated STOP_UNRECORDED_MESSAGE as a failed interrupt and skipped
resolveTurn, leaving the prompt() unresolved although the stream had stopped.
The sentinel moves to src/common/constants/workspace.ts; cancel settles the
prompt as cancelled for Ok and for the sentinel, then reports the durability
failure.
@ibetitsmike

Copy link
Copy Markdown
Contributor Author

@codex review

Round 11 fixes are in 3ae3905: chat error toasts (unrecorded Stop, child budget) are retained per workspace and drained when that workspace's chat input mounts, and ACP cancel settles the pending prompt as cancelled on STOP_UNRECORDED_MESSAGE before reporting the durability failure.

@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: 3ae390538c

ℹ️ 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 withdrawnWakeSend = retiring
? this.inFlightBashMonitorWakeSendsByOwner.get(workspaceId)
: undefined;
const stopSettled = Promise.withResolvers<boolean>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve Stop support on Node 20

When the npm CLI/server runs under the project's accepted Node 20 or 21 runtime—the CLI uses a node shebang and the Makefile only rejects versions below 20—Promise.withResolvers is undefined because it was introduced in Node 22. This executes before session.interruptStream, even when retirement is disabled, so every interrupt is caught as a failure and a live stream cannot be stopped; use a Node-20-compatible deferred promise or raise the supported runtime minimum.

Useful? React with 👍 / 👎.

return;
}
}
this.autoRetryStateUnrecorded = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serialize auto-retry state persistence

When two state changes overlap—for example, provider-config handling starts clearing an old abandon marker and a Stop then records a withdrawn wake—each call performs an asynchronous unlink/write from its own snapshot, but any completion clears the shared autoRetryStateUnrecorded flag. If the newer marker write finishes first and the older unlink finishes last, memory still contains the new marker while disk does not and the flag is false, so recordPendingStartupAutoRetryAbandon() acknowledges the Stop and the trailing row can replay after restart. Serialize or version these writes and only clear the flag when the completed operation still represents current state.

AGENTS.md reference: AGENTS.md:L150-L150

Useful? React with 👍 / 👎.

Comment on lines +35 to +36
for (const message of takeChatErrors(workspaceId)) {
pushToast({ type: "error", message });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve every queued chat error

When two or more errors accumulate while a workspace input is unmounted, this drains the entire retained array synchronously, but the real pushToast in ChatInput writes a single toast state value. React batches those state updates, so only the final message is rendered and every earlier Stop or child-budget warning has already been deleted from pendingByWorkspace; queue the toasts through dismissal or otherwise retain each message until it is actually displayed.

Useful? React with 👍 / 👎.

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