Skip to content

webchat: support mid-turn steering instead of queueing messages sent during a running turn #547

Description

@frozenluo

Problem

In webchat, while a turn is still running, a newly sent message is queued behind
the per-sessionKey serial gate
instead of reaching the running agent. The user has
to wait for the current turn to finish before their new input has any effect — even
when that input is meant to redirect the agent right now ("stop, do it this way
instead", "also check X", "no, not that file").

Today's behavior (daemon serial gate, packages/daemon/src/daemon.ts):

  • dispatchWebchatTurn → admission → the serial gate; if a turn is in flight for the
    same sessionKey, the message is appended to serialQueue and dispatched only
    after the current turn settles.
  • Depth is capped by MAX_QUEUED_PER_SESSION (10); past that the message is rejected
    with reason: 'busy'.
  • If the running turn fails, queued messages are not auto-run at all
    (TurnFailedError — "queued messages were not auto-run").

So the only way to steer today is cancel + resend, which throws away the partial work
of the running turn.

Requested change

Support mid-turn steering for webchat: a message sent while a turn is running
should be delivered into the running turn as additional user input, not parked in
the queue. This is the behavior users already expect from Claude Code / Codex CLI,
where typing during a turn injects the text into the ongoing turn.

Sketch of what needs to happen:

  • Feed the new text to the in-flight ACP turn instead of enqueueing it (agent-side
    support permitting — needs a capability check per runtime; Claude and Codex ACP
    runtimes may differ, and a runtime without support must fall back to today's queue).
  • Keep the serial gate for everything that genuinely must serialize (platform
    ingress, hooks, scheduler wakes) — this is specifically about interactive webchat
    input on an already-running session.
  • Transcript/UI: the steering message should render inline in the running turn, not
    as a separate queued turn, and the composer should stop showing the message as
    "queued".
  • Bound it: cap steering injections per turn, and define what happens to steering
    text that arrives after the turn has already ended (fall back to a normal new turn).

Notes / open questions

  • Does the current ACP wire contract (packages/protocol/src/frames/agent.ts,
    packages/protocol/src/frames/webchat.ts) already have a slot for mid-turn user
    input, or does this need a new frame?
  • Interaction with permission prompts: if the turn is parked on a permission request,
    steering text should probably not be swallowed by the prompt.
  • Fencing (sessionEpoch / seq / launchId) semantics for injected input.
  • Whether !queue stays as the explicit opt-in for "actually queue this, don't steer".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions