acpmux: Send now stops Claude's step, not the turn, so a steer is read at once

Draft
TE
16d ago
+330-9

Problem

Leo wants a Send now on a message steered into a running turn, as Claude Code desktop has (2026-10-20). A steer waits until Claude Code's current step ends, which can be minutes into a long tool call. Send now should stop that step so Claude reads the message right away. Only the user does this; the composer's keys never interrupt (#22741).

acpmux had no way to do it:

  • Stop (hub/cancel.rs) does make Claude Code read an unread steer. But it marks the turn cancelled, so the chain ends "stopped", and its 10 s forced end would cut off the steer while Claude runs it.
  • queue_edit send_now on a queued prompt only steers it in for a steering agent, so it waits for the step too.

Change

  • Claude adapter (claude_stdio/outbound.rs):
    • A steer with _meta.interrupt writes the line and Claude Code's interrupt together.
    • A session/cancel with _meta.steerNow writes the interrupt only while a steer is unread, and does not set the Stop flag.
    • Claude Code then answers the stopped step and reads the steer as one more turn of the prompt. The existing unread-steer path keeps the turn open, and the turn ends the way the agent ends it (end_turn).
  • Hub (queue_edit.rs, steer_end.rs): send_now of a prompt that is no longer queued (already steered in) sends the steerNow cancel. A queued prompt is steered in with interrupt, so it no longer waits either.
    • Only Claude's adapter gets the steerNow cancel, since another agent would take a cancel as Stop. Other agents answer ok: false.
    • There is no forced end; Stop keeps its own.
  • The pane's button comes in a follow-up, next to message-actions' waiting ✓ (#22773). It calls the same queue_edit send_now with the steer's promptId.

Claude Code's order of lines, observed live with claude -p --input-format stream-json --replay-user-messages (2.1, haiku): a steer written during sleep 25, then an interrupt. The stopped step's error_during_execution result came first. Then the steer's echo, its reply, and a success result. fake_claude.py's new "long-step" replays that order.

Verification

  • cargo test -p acpmux --lib claude_stdio::steer_now_tests: adapter tests for steer-with-interrupt, steerNow with and without an unread steer, and Stop still ending cancelled.
  • cargo test -p acpmux --test hub_integration steer_now: end to end through the hub with fake_claude.py. Send now on a waiting steer and on a queued prompt; the agent reads it and the turn ends end_turn. Stop still ends cancelled.
  • Test commit ad34433a first, then the fix 715aca9f. Fleet results go below.

Observed / Not run

  • Observed: Claude Code's line order (above), live.
  • Not run yet: the cargo tests. They run on the fleet, never on Leo's Mac; results will be added here.
  • Known edge: Send now while a permission prompt is open stops the step, but this PR does not withdraw the prompt's card. Stop does withdraw it.

Changelog

Changed: Send now on a waiting message stops the agent's current step so it reads the message at once; the turn keeps going.

Agent session: cosmic-gray


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Send now on a message steered into a running turn now stops Claude Code's current step so the message is read immediately, instead of waiting until the step ends (which could be minutes into a long tool call). The turn keeps running and ends as the agent ends it, not as stopped.

  • A steer with _meta.interrupt is written together with Claude Code's interrupt; the turn continues and ends with end_turn.
  • send_now on an already-steered prompt sends a steerNow cancel, and a queued prompt is now steered in with interrupt, so neither waits.
  • Only the Claude adapter gets the steerNow cancel; other agents answer ok: false since their cancel would mean Stop. Stop keeps its forced end.
  • The pane's Send now button is a follow-up; it calls the same queue_edit send_now with the steer's promptId.