Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Set Settings → General → Follow-up behavior to Steer.
- In a Claude thread, ask the agent to run a command that takes a minute or more in the foreground. Claude Code refuses a bare
sleep, so I usedcurlagainst a local endpoint that does not answer. The first time I hit this, it was a slowduscan. - Wait until the command has run for about 5 seconds, then send a follow-up, for example:
Codeword: PELICAN. Remember it, I will ask for it later.It shows in the thread as a steer. The agent does not react, because the command is still running. - Press Stop before the command finishes.
- Send:
Earlier in this conversation, did I send you a codeword? If yes, reply with exactly CODEWORD=<the codeword>. If not, reply with exactly NO_CODEWORD.
The timing in step 3 matters. About 3 seconds after a foreground Bash call starts, Claude Code registers it as a task (task_started). A steer that arrives before that aborts the call and is read at once, so nothing is lost. A steer that arrives after that waits until the call returns.
I scripted these steps against a fresh server and a disposable Git project, sending the same message.dispatch (steer_active) and run.interrupt commands as the web client: repro-steer-stop.ts. Results on claude-sonnet-5-5:
| Run | After the steer | Answer to step 5 |
|---|---|---|
| Stop, 3 runs | Command still running 5 s after the steer reached the CLI, then Stop | NO_CODEWORD every time |
| Control, no Stop | Command released 5 s after the steer reached the CLI | CODEWORD=PELICAN-60C6 |
Expected behavior
A steer that the thread shows as sent either reaches the agent, or the thread shows that it did not, for example by returning it to the composer or the queue when Stop drops it.
Actual behavior
The steer stays in the stopped run and looks delivered, but Claude never reads it. The next turn answers NO_CODEWORD, and nothing tells the user the message was dropped.
What happens, from the provider log and Claude's session file:
- The server offers the steer to the Claude CLI with
priority: "now"(steerTurninClaudeAdapterV2.ts). The Bash call is already a task, so the CLI keeps the steer in its in-memory queue. The session file records only aqueue-operationenqueuefor it, with nodequeueand no user message. - Stop runs
query.interruptand thenquery.close(interruptTurn), which ends the CLI process with the steer still queued. - The next turn resumes the session in a new process and offers only the new prompt. The queued steer is not part of the resumed conversation.
- The steer is offered without a
uuid. Prompts that carry one getcommand_lifecycleevents (queued,started,completed,cancelled), so the adapter could tell whether Claude read the steer before Stop. Today it cannot.
The V1 Cursor adapter had the reverse problem, a steer that still ran after Stop. #13423 fixed it by dropping the queued steer, and that PR was closed during the V2 migration. If dropping an unread steer is the intended Stop behavior, the bug here is only that the drop is silent.
Impact
Major degradation or frequent failure
Version or commit
main @ 4ee6bfd. First seen on Nightly 0.0.46-nightly.20261004.2644.
Environment
macOS 27.0.1, Node 26.9.0, Claude Code 2.1.289. Seen with claude-opus-5-5 and reproduced with claude-sonnet-5-5.
Logs or stack traces
# Native Claude protocol log of one Stop run (outgoing = server to CLI)
16:25:42.732 in {"type": "assistant", "content": ["tool_use:Bash"]}
16:25:46.160 in {"type": "system", "subtype": "task_started", "task_type": "local_bash", "is_backgrounded": false}
16:25:47.402 out {"type": "prompt.offer", "content": "Codeword: PELICAN-C362. Remember it, I will ask …", "priority": "now", "uuid": null}
16:25:53.686 out {"type": "query.interrupt"}
16:25:53.687 out {"type": "query.close"}
16:26:04.061 out {"type": "query.open", "resume": true}
16:26:04.064 out {"type": "prompt.offer", "content": "Earlier in this conversation, did I send you a c…", "uuid": "795ee797…"}
16:26:06.918 in {"type": "command_lifecycle", "state": "queued", "command_uuid": "795ee797…"}
16:26:07.675 in {"type": "command_lifecycle", "state": "started", "command_uuid": "795ee797…"}
16:26:09.292 in {"type": "assistant", "content": ["text:NO_CODEWORD"]}
# Claude session file of the same run: the steer was only ever queued
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-10-04T16:25:47.406Z","content":"Codeword: PELICAN-C362. Remember it, I will ask for it later."}Screenshots, recordings, or supporting files
steer-lost-on-stop.mp4
The web client against a fresh dev server and a demo project, with Claude Sonnet 5.5 and Follow-up behavior set to Steer. The recording script overlaid the yellow captions; they do not change the app. The agent's command is curl against a local endpoint that never answers, so it stays in the foreground until Stop.
repro-steer-stop.ts runs the same flow without a browser. Save it as apps/server/scripts/repro-steer-stop.ts and run node apps/server/scripts/repro-steer-stop.ts, or add --control for the run without Stop. It uses the existing Claude CLI login and keeps the server log, provider log and projection in the directory it prints.
Workaround
Wait for the running command to finish before pressing Stop, or send the message again after stopping.
Activity
juliusmarminge commentedon Oct 4, 2026
Note
Grok responding on behalf of Julius.
Triage
Thanks for the scripted repro, the control run, and the protocol log, @nekohasekai. They made this straightforward to trace.
What I found
This only happens after Claude emits
task_startedfor a foregroundlocal_bashcall. A steer sent before that point aborts the call and is read in the same process. A steer sent after it waits in Claude's in-memory queue until the tool returns. If Stop is pressed during that wait, the CLI process exits with the steer still in its queue.dispatchSteerIntoRuninOrchestrator.tswrites the steer into the active run as a completed user message. The timeline shows "Steered the active turn" as soon as the offer is accepted, before Claude has read it.ClaudeAdapterV2.steerTurnoffers the text withpriority: "now"and nouuid. Prompts fromstartTurngetclaudePromptUuid(attemptId), and the adapter already readscommand_lifecyclefor that uuid. A steer has no uuid, so the adapter can't tell a queued steer from one Claude has started.run.interrupt,holdQueue: true) callsquery.interruptand thenquery.close, which ends the process.holdQueueonly holds later queued runs and doesn't cover a steer already attached to the running run.#13423 dealt with the reverse case, a steer still running after Stop. It was closed during the V2 migration and doesn't cover this case.
Likely fix area
Some options:
uuidsocommand_lifecyclecan show whether Claude actually started reading one before Stop.A maintainer will decide on the fix direction.
nkoynov commentedon Oct 18, 2026
The same thing happens on OpenCode 2 threads, by design: pressing Stop deletes any steer OpenCode hasn't read yet. The adapter collects it in
strandedSteersand callssession.inbox.cancelbefore the next prompt (from #14468), and the bubble stays in the stopped run looking sent.Repro (OpenCode 2.0.29+, Follow-up behavior set to Steer): ask the agent to run
sleep 60 && echo donein the foreground; while it runs, sendCodeword: PELICAN. Remember it.(OpenCode reads steers only at a step boundary, so it waits behind the command); press Stop, then askDid I send you a codeword earlier?The agent says no.Cancelling it makes sense, since otherwise OpenCode would slip the old steer in ahead of the next prompt. What I'd change is that the text is lost. Of the options in the triage, I'd go with returning it to the composer: after a Stop you usually want to rethink what to send, and the composer lets you edit, resend or drop it without an extra step. Roughly:
turn.terminalgets an optional list of message ids the provider took back (packages/provider-core/src/server/ProviderAdapter.ts). The OpenCode 2 adapter fills it from the turn's undelivered steers when an interrupted turn ends (packages/provider-opencode/src/server/v2/adapter.ts); each inbox id is already built from the T3 message id.rewindToMessage/ "Restore prompt".Returning them to the queue as held items would be smaller (server only, since Stop already holds the queue and the queue is editable), but it takes an extra step and resuming sends them as they were.
Claude could fill the same field once its steers carry a uuid, as suggested above; I'd keep that separate. If this direction works for you, I can open a small PR for the OpenCode 2 part.
juliusmarminge commentedon Oct 22, 2026
Reproduced on main b5a226e49c, Ubuntu Linux, Node 24.21.0, Claude Code 2.1.303 / Sonnet 5.5, using the real web dev app and a disposable Git project.
With Follow-up behavior set to Steer, I ran foreground
curlagainst a held local endpoint, sent the codeword aftertask_started, pressed Stop, waited for the run to end, then asked for the codeword. Claude answeredNO_CODEWORD; the steer remained visible without an undelivered indication. Control: releasing the endpoint without Stop producedCODEWORD=PELICAN-CONTROL-15708.Protocol transcript. Related #15720 covers delayed delivery; this report covers the silent loss on Stop.