Files
buzz/crates
Logan Johnson 0a070dfd3f fix(acp): steer mid-turn DM follow-ups natively again (#8058)
🤖
## Summary
Some agents can take a new message into a turn that is already running.
This is called "native steering": the agent keeps working and folds the
new message in, instead of stopping and starting over.

#6132 made this safer in channels: a message is steered in only when it
belongs where the running turn will reply. In a channel, each top-level
message gets its own reply thread, so a second top-level message must
not be steered into a turn that replies in the first one's thread.

That rule also ran in DMs, where it does not fit. In a DM, a top-level
message is answered at the top of the conversation, not in a new thread.
So every top-level DM message replies in the same place. But the rule
treated each one as its own thread. As a result, **every DM follow-up
sent while the agent was working cancelled and restarted the turn**,
instead of being steered in as it was before #6132.

Now:
- **DMs:** a follow-up is steered natively when it replies in the same
place as the running turn: both top-level, or both in the same thread. A
thread reply during a top-level DM turn (or the reverse) still takes the
cancel-and-restart path, because the running turn would reply in the
wrong place.
- **Channels:** unchanged from #6132.

## Details
- The queue now records each running turn's reply route: its thread root
(or none) and its reply thread. One function,
`ReplyRoute::accepts_steer`, decides whether a new message may be
steered in. It compares thread roots in DMs and reply threads in
channels.
- The DM rule mirrors how the prompt is built: a top-level DM's
`<context>` has no `--reply-to` target, and a threaded DM's target is in
its thread.
- The steer decision must agree with how the running turn's prompt was
written. The prompt task records whether it writes the prompt as a DM,
and the steer decision reads that recorded value. So if channel metadata
was missing when the turn started (the prompt then uses the normal
channel form) and loads later, the DM rule is not applied to that turn.
- The prompt task records this value as soon as channel metadata
resolves, before session setup. A follow-up sent during a slow
`session/new` is steered in, as on main. When an `initial_message` setup
prompt runs first, the value is recorded after that setup turn, because
the setup prompt reads the same steer mailbox.
- Before the value is recorded, the steer decision uses the listener's
own channel classification. A channel the listener knows is not a DM is
always written in channel form, so the channel rule applies at once,
exactly as on main. A DM, or a channel whose type is not resolved, takes
the cancel-and-restart path until the value is recorded.

### Related issue
Follow-up to #6132 (merged). Found in review after merge. No separate
issue.

### Testing
- New tests drive DM messages through the listener's real steer decision
with a running turn. A second top-level DM message is steered natively
with no cancel. A DM thread reply during a top-level DM turn is not
steered and takes cancel-and-restart.
- Direct tests of the steer rule cover channel and DM cases: same
thread, different thread, top-level after top-level, and top-level mixed
with thread replies.
- With the DM rule disabled, the DM rule test and the top-level DM
listener test fail.
- Listener tests also cover a DM turn whose prompt was written as a
channel (top-level follow-up is not steered) and a DM follow-up that
arrives before the value is recorded (not steered).
- Production-path tests dispatch a real turn through `dispatch_pending`
and `run_prompt_task` to an agent that holds at `session/new` or
`session/prompt`, then send a follow-up through the real listener. A
top-level DM follow-up during the prompt or during `session/new` is
steered with no cancel. A DM follow-up before an `initial_message` setup
turn is not steered. A same-thread channel follow-up during setup is
steered with no cancel.
- Mutation checks: each fails its test. They are: not recording the
value in `run_prompt_task`, passing a detached value from dispatch, not
recording it before session setup, recording it before an
`initial_message` turn, and removing the known-channel rule for the
unrecorded case.
- `cargo test -p buzz-acp` passes on the rebased head, with the
timing-sensitive `acp::tests` run alone (`--test-threads=1`; this PR
does not change them, and they fail only under full parallel load on
this laptop). `cargo clippy -p buzz-acp --tests -D warnings` and `just
desktop-tauri-clippy` are clean.

---------

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-10-05 14:43:21 -04:00
..