Files
qwen-code/docs/developers/daemon
Shaojin Wen 23274922f5 feat(web-shell): add Send now for queued mid-turn messages (#13681)
* feat(web-shell): add Send now for queued mid-turn messages

A message typed while a turn runs waits for the model response that is
streaming and for its tool batch, so a correction typed into a long answer
reached the model seconds later, and Stop was the only faster path.

Typing keeps that behavior: the message stays queued, editable and
deletable until the next tool boundary. A new Send now action on a queued
row asks the daemon to deliver the queued messages at once.
POST /session/:id/mid-turn-messages/send-now, advertised by
session_mid_turn_send_now, sends the agent craft/midTurnSendNow. While a
foreground turn streams a response that has not started a tool call, the
agent drains the user's messages, cuts the response short, keeps the part
already streamed, and answers in the same turn. Running tools are never
interrupted, a response that started a tool call is never cut, queue-only
steering is not taken early, and a request is scoped to the turn that
received it. Input taken by a turn that is then stopped or fails is kept
in that turn's conversation.

The TypeScript SDK gains sendMidTurnMessagesNow on both clients.

* fix(web-shell): address review findings on Send now

- Restart the Stop-hook state when a continuation answers input that
  cut its response short, so the next Stop check is not hook-forced and
  the consecutive-block count and reasons start over.
- Give input a finished turn keeps unsent a 2 s resolution deadline of
  its own; the deadline also cancels media bridges.
- Requeue a failed send-now drain in its original order, and log a
  send-now request the agent rejects.
- In Web Shell, reconcile a row whose message already left the queue
  and ignore a repeated click while a request is out.
- State the tool-call carve-out wherever Send now is described, and
  document goal turns, the narrow no-answer window and token accounting.
- Add tests for the review's gaps, drive the drain deadline with fake
  timers, and add the route to the multi-workspace failure tables.

* fix(web-shell): keep send-now input a stopped tool boundary leaves unbuilt

When a turn is stopped while a tool-boundary drain resolves a message,
the drain now hands back the messages it has not built that an early
send-now drain took, so the end-of-turn keep records them. Input the
boundary drain took for itself is still dropped there, as before.

- Tests: the stopped boundary keeps that input once and carries nothing
  into the next turn; a Goal continuation is cut; a background
  notification turn is not; a failed send-now drain does not requeue a
  message that left the queue meanwhile.
- Bridge and SDK contracts: the cut applies to a foreground turn's
  streaming response, and `requested: true` does not mean a cut.
- Protocol and design docs: the timed-out send-now drain, resending a
  settled id, the 10-message drain limit, and the tool-boundary drop.

* test(web-shell): pin send-now behavior questioned in review

- Assert that cron and background-notification turns never open their
  response to interruption and that a send-now request arms nothing
  while they stream.
- A response cut short by queued input still answers that input when it
  ends with an error, while an error from a response its drain did not
  cut remains the turn's error. Explain in `watch` and the design doc why
  the cut response's error is absorbed: the cut does not always arrive as
  an AbortError, and the response was given up.
- A message whose deletion missed while a failing drain held it is still
  delivered, which matches the `{ removed: false }` answer the client
  got. Document this in the protocol reference.
- Rename a requeue test to say it covers a queue-only entry.

* docs(web-shell): scope the abort invariant and drain-held removals

- The main prompt loop's catch states that other AbortErrors surface.
  Note that a response cut short by queued input reaches that catch only
  when the turn itself is stopped meanwhile, because `watch` absorbs
  whatever a cut response throws.
- In the force-drain paragraph, add the drain-held case of
  `{ removed: false }`. Such an id must not be re-posted, and its listing
  in `settledMessageIds` is not final, because a failed drain returns it
  to the queue without an event.
2026-10-11 06:24:48 +00:00
..