🤖 fix: defer bash-monitor wakes until the active turn ends - #4114
🤖 fix: defer bash-monitor wakes until the active turn ends#4114ibetitsmike wants to merge 16 commits into
Conversation
|
@codex review |
This comment has been minimized.
This comment has been minimized.
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: 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".
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
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: 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".
…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.
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: 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".
…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.
|
@codex review Round 4 (a8f06c1). Addressed the four round 3 findings:
Each fix has a red-green test; the ACP prompt-correlation expectation is updated for the |
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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.
…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.
|
@codex review Round 5 (head 1f3cc0b). Addresses both Round 4 findings:
|
There was a problem hiding this comment.
💡 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".
This comment has been minimized.
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.
There was a problem hiding this comment.
💡 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".
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.
|
Codex Review: Didn't find any major issues. Delightful! 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.
… 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.
There was a problem hiding this comment.
💡 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".
… 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.
|
@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. |
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: 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".
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.
|
@codex review |
There was a problem hiding this comment.
💡 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".
…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
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
xum/src/node/services/bashMonitorWakeReconciler.ts
Lines 656 to 659 in f8223a3
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".
…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.
|
Re the review-body finding "Preserve terminal signals created after Stop" (bashMonitorWakeReconciler.ts, Tests (bashMonitorWakeReconciler.test.ts): |
|
@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. |
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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.
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.
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: 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".
…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.
There was a problem hiding this comment.
💡 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>(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 👍 / 👎.
| for (const message of takeChatErrors(workspaceId)) { | ||
| pushToast({ type: "error", message }); |
There was a problem hiding this comment.
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 👍 / 👎.
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 owntask_awaithad 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, schedulescheduleBashMonitorWakeReconcileAfterIdleand return"deferred"; otherwise send the wake withrequireIdle: true(noqueueDispatchMode: "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.interruptStreamwith the newretireBashMonitorAttentionoption (passed by the user Stop entry points: the Stop button, the Escape keybind, the command palette Stop and ACPcancel; internal interrupts such as goal promotion, archive, ACP disconnect and send-now leave monitor output owed): before the interrupt,BashMonitorWakeReconciler.consumeCurrentwithdraws 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 becausesession.interruptStreamreturns only after the abort settled, when an idle-triggered dispatch may already be admitting; it does not takebashMonitorHistoryLocks, 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 butinterruptStreamreturnsErr(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 throughstopStream(), 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 ACPcancelsettles the pending prompt as cancelled for that sentinel too (the stream did stop) before reporting the durability failure;STOP_UNRECORDED_MESSAGElives incommon/constants/workspace.tsfor that. Monitors stay registered and later output still wakes.consumeCurrentbecomes public; the now-unreaddedupeKeyfield is dropped fromBashMonitorWakeDispatch.AgentSession.sendMessage: a cancelable wake whose signal fires after the acceptance point of no return (its row is already durable) but before PREPARING now resolvesOkwithout starting a stream, the same contract ascancelBeforeAcceptanceand 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.interruptStreamjoins the in-flight wake send and, if the marker write failed, retries it (recordPendingStartupAutoRetryAbandon) before reporting the Stop recorded.MessageQueue:hasAllWorkspaceTurnContinuations,hasAllWorkspaceTurnContinuationsAheadOfPromotedToolEnd,hasNextWorkspaceTurnContinuation,getNextQueueCutCandidateandisNextEntryBashMonitorWakenow read the first entry whose cancel signal has not fired (the rulegetNextDispatchableModealready used), so a withdrawn entry can neither supersede nor misattribute a delegated turn's correlation.StreamManager.cleanupAbortedStreamaddsmodel: streamInfo.modeland the request-pinnedmetadataModelto thestream-abortmetadata (schema gains both as optional), andAgentSession.handleTurnAbortprefers the effective model over the requested model string and passesmetadataModelinto 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
workspaceService.test.tsdrive a fake SDK stream throughAIService.streamMessageand 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 ownsbashMonitorHistoryLocks, and returnsSTOP_UNRECORDED_MESSAGEwhen 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;stopStreamretains anErrStop for the workspace's chat input (shown on mount, after unmount, and not for another workspace) and stays silent onOk; an unrecorded Stop still settles the ACP prompt as cancelled whilecancelreports the failure; an interrupt withoutretireBashMonitorAttentionkeeps 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.metadata.modelfallback or themetadataModelpass-through fails the newstreamManagerandagentSession.queueDispatchcases (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.claude-sonnet-5through 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 bytask_awaitended 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; nofinishReason: "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 onmain, not touched here).Risks
Moderate, scoped to bash-monitor wakes and turn correlation.
task_await; it is only the unsolicited wake that moves later.session.interruptStreamreturnsOkeven 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 passretireBashMonitorAttentionretire 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.Pains
The branch was rebased across #4109 (turn lifecycle centralization), which required porting the test harnesses from direct
aiEmitterevents to settlingTurnStreamHandle.completion. Locally,bunon PATH resolved to 1.2.15 while the repo pins 1.3.5; under 1.2.15 the injected-failure tests instreamManager.test.tsandagentSession.queueDispatch.test.tsfail identically onmain, which cost a diagnosis round.Generated with
xum• Model:anthropic:claude-fable-5-1• Thinking:xhigh• Cost:$753.56