feat(buzz-acp): steering as the default mid-turn mention delivery#1160
Open
tlongwell-block wants to merge 3 commits into
Open
feat(buzz-acp): steering as the default mid-turn mention delivery#1160tlongwell-block wants to merge 3 commits into
tlongwell-block wants to merge 3 commits into
Conversation
When a new, already-author-gated mention arrives while the agent is
mid-turn, deliver it by cancelling and re-prompting with a steering
framing: the message arrived while the agent was working, so it should
continue its in-progress task and weave the message in — rather than
treating it as a replacement request.
Builds on the existing cancel+merge machinery (--multiple-event-handling).
The eligible author set (owner ∪ allowlist ∪ siblings) is already enforced
by the inbound author gate, so every event reaching the mode gate is
steering-eligible.
- Add MultipleEventHandling::Steer; make it the default (was queue).
Steer requires --dedup=queue (default), enforced by extracted
validate_multiple_event_handling().
- Add ControlSignal::Steer and CancelReason {Interrupt, Steer}; carry the
reason on FlushBatch from the pool cancel path through requeue into
format_prompt.
- format_prompt selects wording via MergeFraming: steer vs interrupt.
- Extract the mode-gate decision into mode_gate_signal() for testability.
Tests: framing (incl. multi-event), reason propagation, gate mapping,
config default and dedup validation. 350 pass, clippy clean.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…or_header Dawn's framing review: the steer prior-section header overclaimed captured partial work, but session/cancel is terminal — the section actually holds the original request. Swap to [What you were working on], honest about its contents. Add coverage for the seams the split unit tests miss: - queue->render end-to-end: real flush_next merge driven through the real format_prompt, asserting steer framing survives the whole path. - default OwnerOnly author gate: a stranger is dropped (so it can never reach the mode gate to steer); owner and sibling are admitted. Pins the 'ineligible author must NOT steer' invariant against the default mode. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
… steer; doc fix Addresses Perci's clean-context review: - FlushBatch::cancel_reason doc said None => Interrupt framing, but MergeFraming::for_reason(None) defaults to Steer. Fixed the doc, and aligned the defensive lib.rs fallback (unwrap_or) from Interrupt to Steer so both unset-reason paths agree on the gentler default. The path always sets a reason; this is purely defensive consistency. - Pin the cross-thread edge: original work in thread A, steering message in thread B (same channel). The reply instruction targets the steering message (where the mentioner waits) while framing still says 'continue your in-progress work' — intended behavior, now asserted. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Makes steering the default way a new mention reaches an agent mid-turn. When an eligible author (the agent's owner, an allowlisted user, or a verified sibling bot) @mentions the agent while it's working, that message is delivered between calls with light framing — "this arrived while you were working, continue" — instead of waiting silently until the turn ends.
A new
MultipleEventHandling::Steervariant becomes the default (wasqueue). On a steering-eligible event arriving for an in-flight channel, the harness cancels the current turn and immediately re-prompts with the original work plus the new message, framed to weave in and continue rather than supersede.Why this shape (and the honest ceiling)
We are the ACP client. The protocol gives a client exactly one mid-turn signal —
session/cancel— and it is terminal. There is no "inject and continue" primitive across the ACP boundary (confirmed against the ACP v2 spec; Zed #55809 shows even Claude Code's SDK delivers aUserMessageChunkinto a running session but the harness renders it without advancing the turn — latent transport, no continue semantics). So true non-interrupting injection inside the agent's loop is not reachable over ACP.Cancel + merge + steering-framing is therefore the ceiling, and it works on every agent today (goose / claude-agent-acp / codex). Most of the machinery already existed from PR #216's cancel+re-prompt (
interrupt/owner-interrupt); this PR adds a framing variant and flips the default.Changes (4 files, all
buzz-acp)config.rs—MultipleEventHandling::Steer; default flippedqueue→steer;validate_multiple_event_handling()(Steer/Interrupt/OwnerInterrupt requireDedupMode::Queue, the default).pool.rs—ControlSignal::Steer;requeue_cancelled_batch()maps signal →CancelReasonand stamps it on the requeuedFlushBatch.queue.rs—CancelReason {Steer, Interrupt}onFlushBatch;cancel_reasonsmap threads the reason throughrequeue_as_cancelled→flush_next;MergeFraming::for_reason()selects steer vs. interrupt wording informat_prompt. Steer framing: prior block[What you were working on], new block "arrived while you were working", closing note "Continue your in-progress work and incorporate it if relevant; if unrelated, briefly acknowledge and carry on."lib.rs—mode_gate_signal()maps handling + author + owner →Option<ControlSignal>; threadscancel_reasoninto the requeue.Eligibility / security
Author eligibility (owner ∪ allowlist ∪ verified siblings) is enforced upstream by the inbound author gate (
author_allowed) — ineligible authors are dropped before they reach the mode gate, so they can never steer. "Sibling" is a cryptographically-verified same-owner claim via the NIP-OA auth tag (is_owner_or_sibling), not a heuristic. The inject channel is also an injection-attack surface, so this authenticated definition is load-bearing.Config-composition note: steering fires for whatever
--respond-toalready admits. Under the defaultrespond-to=owner-only, the eligible set is owner ∪ siblings; allowlisted "allowed users" are included when deployed with--respond-to=allowlist. Owner+siblings is the safe default; allowlist stays opt-in (widening the default gate would widen the inject surface for every deployment).Framing wording
The steer
prior_headeris[What you were working on], deliberately not "your in-progress work" —session/cancelis terminal and returns no partial work, so that section holds the original request, not a transcript. The honest header avoids implying we preserved state we didn't. (Framing reviewed by Dawn; soft-guidance register per SWE-PRM 2509.02360.)Tests
353 pass / 0 fail on
buzz-acp; clippy + fmt clean. New coverage includes: steer vs. interrupt framing informat_prompt, multi-event header branch, reason propagation through requeue, no-reason → steer fallback, gate → signal mapping, config default + validation, a queue→render end-to-end steer test, and the negative case (defaultOwnerOnlydrops a stranger so it can never steer; admits owner + sibling).Behavior change
This flips the global default from
queuetosteer— a deliberate behavior change for all deployments, per the "make steering the default" requirement.Known properties (non-blocking)
Reviewers
Dawn (framing wording), Perci (clean-context correctness pass — no blocking issue, all three follow-ups landed). Research lane (arXiv + ACP spec + GitHub/Zed prior art) established the protocol ceiling.