Commit fbc8365
committed
fix(orchestrator): Keep agent output attached after a mid-turn steer
Steering a Claude turn while a tool was running settled the run while the
agent was still working. The reply to the steer then had no live turn to
attach to, so it was buffered and, with nothing asking for a continuation,
silently dropped: the thread went quiet and returned to Ready with the
steer unanswered.
Two independent defects produced that.
The steer guard keyed on one exact string. `isClaudeActiveSteeringAbortResult`
matched only `terminal_reason: "aborted_streaming"`, which the CLI reports
when a steer interrupts a streaming assistant message. A steer that lands
while a tool is running is queued instead and delivered when the tool result
arrives, and the CLI ends that native turn with `aborted_tools`, answering
the steer in the next one. That result did not match, so the turn finalized
while the CLI kept working. A newly recorded fixture captures the shape from
a real session: `subtype: success`, `terminal_reason: "aborted_tools"`,
empty text, followed by a fresh native turn carrying the answer.
`isClaudeSteeringHandoffResult` replaces it. While a steer is outstanding, a
result hands off when the CLI cut the turn short (`aborted_streaming` or
`aborted_tools`, including on the error result that is how the first of those
arrives) or when the turn ended cleanly with nothing to say. A result
carrying text has answered something and still terminalizes, so a steer the
CLI absorbs into the running turn does not hold the run open; `max_turns`,
`background_requested`, `tool_deferred` and the hook reasons are real
terminals even as an empty success. Each accepted steer swallows exactly one
result, so the turn still ends on the result that answers it, and the
handoff is decided before anything else reads the result so it cannot seed
the turn's fallback assistant text.
Because a handoff makes the turn depend on a native turn that has not opened
yet, it is bounded: if no frame reaches the turn within 60 seconds it settles
as a failure rather than leaving the run running for the rest of the session.
A failure, not a quiet completion, because an accepted steer that was never
answered is exactly the silence this change exists to stop.
Buffered assistant text could also be stranded. When a run terminalizes
early while the query is still live, later frames go to the wake buffer,
which only requested a continuation for a tracked task notification, a
running subagent, or a result. Ordinary post-settle assistant text asked for
nothing, so it sat in the buffer until the session recycled. Assistant text
now requests a continuation on its own. Tool_use and tool_result frames stay
gated: they are the model working rather than speaking, and the notification
that follows them carries the wake detail.
Also record replay `stream_event` frames, which every query receives since
`includePartialMessages` landed, and allow a fixture to hold its steer until
the target run reports a given turn item so a mid-tool steer can be replayed
deterministically.1 parent 71e5450 commit fbc8365
10 files changed
Lines changed: 832 additions & 18 deletions
File tree
- apps
- server
- scripts
- src/orchestration-v2
- Adapters
- testkit/fixtures
- message_steering_mid_tool
- web/src/hooks
Lines changed: 10 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
91 | 92 | | |
92 | 93 | | |
93 | 94 | | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
94 | 101 | | |
95 | 102 | | |
96 | 103 | | |
| |||
240 | 247 | | |
241 | 248 | | |
242 | 249 | | |
| 250 | + | |
243 | 251 | | |
244 | 252 | | |
245 | 253 | | |
| |||
259 | 267 | | |
260 | 268 | | |
261 | 269 | | |
| 270 | + | |
262 | 271 | | |
263 | 272 | | |
264 | 273 | | |
265 | 274 | | |
266 | 275 | | |
267 | 276 | | |
268 | | - | |
| 277 | + | |
269 | 278 | | |
270 | 279 | | |
271 | 280 | | |
| |||
0 commit comments