fix(chat): queue plain prompts while a turn waits on background work

Merged
HA
2d ago
+88-41

Problem

A follow-up sent while the agent was finishing could leave the chat showing "Working for …" with the message in the transcript but never answered. Every message after it then waited in the queue. Investigation: https://app.superset.sh/page/chat-stuck-after-interrupt-60dhm7

Since #8207, LiveSession.prompt steered a plain Enter into the running turn whenever the turn was waiting only on background work (awaitingBackground). steerQueueIntoHeldTurn also steered the whole queue once that wait began. claude-agent-acp (0.85.0, CLI 2.1.286) accepts the steer (injected) and then holds session/prompt open until a result answers it. If none does, the adapter keeps the turn running, and isBusy() queues every later prompt behind it.

Change

  • A prompt goes into a running turn only when it names one: Cmd+Enter (steer.expectedTurnId), or "send now" on a queued prompt (steerQueuedPrompt).
  • Plain Enter during a background wait now queues, the same as during any other running turn.
  • Removed steerQueueIntoHeldTurn, so queued prompts are no longer moved into the turn when the wait begins.
  • Composer: a background wait gets its own PromptInput status, "background". The send button then reads "Queue message", with the existing "Queue; ⌘↩ to steer the running turn" tooltip, and Stop stays hidden. #9332's "streaming" without onStop ("Starting the agent…") is unchanged. No new strings.

Trade-off: a plain Enter during a background wait now runs after the background work ends, not right away. To send it now, use Cmd+Enter or Steer on the queued row.

This is a stopgap. It lowers how often a chat enters claude-agent-acp's steer lane. It does not remove the stall: Cmd+Enter and Steer still go through _session/steering. Follow-ups:

  • SUPER-3168: a watchdog that re-queues and resends a steer that gets no answer, and the upstream claude-agent-acp report. With it in place, plain Enter during a hold could steer again (#8207's behaviour).
  • SUPER-3169: stop the "Working for…" clock during a background wait.

Tests

  • New: a plain prompt during a background wait is queued, never reaches steer(), and starts the next turn (t2) once the held turn ends.
  • New: a prompt sent with steer during a background wait joins the turn.
  • Changed: prompts queued before the wait stay queued in order (this test used to assert they joined the turn).
  • The fallback, ordering, pending-steer and discard tests that ran on the background-wait script now send with steer, so they still cover the steer path.
  • Both new tests fail without the change. bun test in chat-runtime: 452 pass, 2 skip. chat-ui: 84 pass. Typecheck (chat-runtime, chat-ui, desktop) and biome are clean.

Desktop verification

Run on 2026-11-05 in the dev desktop app with a real claude-agent-acp 0.85.0 turn, driven over CDP. The hold comes from a background subagent; a background Bash command does not hold the turn.

Case, while the turn waits on a background subagentResult
Plain EnterQueued; not steered
Subagent finishesTurn ends, then the queued prompt runs and is answered
Cmd+Enter (Ctrl+Enter on Linux)Joins the turn, answered in about 5 s
Steer on a queued rowJoins the turn, answered in about 5 s
Prompt after the turn endsAnswered; nothing stuck
Send button during the wait"Queue message" with the steer tooltip; Stop hidden. Back to "Send message" after the turn ends. Stop still shows during foreground work

Not reproduced: the stuck turn itself. On main, a plain Enter during the hold was steered, answered, and the turn ended normally. The investigation names a steer that lands during a foreground command as the likely trigger. We do not know if the reporter pressed Enter or Cmd+Enter.


Summary by cubic

Fixes a stuck chat state where a prompt sent while a turn only waited on background work was steered into that turn, leaving the session showing "Working for …" with the message never answered and every later prompt queued behind it. Plain prompts now queue during a background wait, and only an explicit steer (Cmd+Enter, or "send now" on a queued prompt) joins the running turn.

Behavior

  • Prompts already queued before the wait stay queued in order instead of being injected into the turn when the wait starts.
  • A plain Enter during a background wait runs after the background work ends; Cmd+Enter or "send now" runs it immediately.
  • The background hold gets its own PromptInput status, "background": the composer button reads "Queue message" with the "Queue; ⌘↩ to steer the running turn" tooltip, and Stop stays hidden since there is no onStop handler.

Summary by CodeRabbit

  • Bug Fixes
    • Prompts without an explicit target stay queued while a turn is awaiting background work, instead of joining it automatically. Explicitly targeted prompts can still join a turn when it accepts steering; otherwise, they remain queued in order.
    • The composer now distinguishes background work from ready and streaming states, and its send button indicates when a message will be queued.
    • Queue and steering guidance appears during background work and when steering is available.