Skip to content

feat(buzz-acp): steering as the default mid-turn mention delivery#1160

Open
tlongwell-block wants to merge 3 commits into
mainfrom
eva/steering-default-mention
Open

feat(buzz-acp): steering as the default mid-turn mention delivery#1160
tlongwell-block wants to merge 3 commits into
mainfrom
eva/steering-default-mention

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

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::Steer variant becomes the default (was queue). 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 a UserMessageChunk into 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.rsMultipleEventHandling::Steer; default flipped queuesteer; validate_multiple_event_handling() (Steer/Interrupt/OwnerInterrupt require DedupMode::Queue, the default).
  • pool.rsControlSignal::Steer; requeue_cancelled_batch() maps signal → CancelReason and stamps it on the requeued FlushBatch.
  • queue.rsCancelReason {Steer, Interrupt} on FlushBatch; cancel_reasons map threads the reason through requeue_as_cancelledflush_next; MergeFraming::for_reason() selects steer vs. interrupt wording in format_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.rsmode_gate_signal() maps handling + author + owner → Option<ControlSignal>; threads cancel_reason into 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-to already admits. Under the default respond-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_header is [What you were working on], deliberately not "your in-progress work" — session/cancel is 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 in format_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 (default OwnerOnly drops a stranger so it can never steer; admits owner + sibling).

Behavior change

This flips the global default from queue to steer — a deliberate behavior change for all deployments, per the "make steering the default" requirement.

Known properties (non-blocking)

  • Sibling-cache staleness: a positive sibling verification is cached until process restart, so an agent once verified as same-owner stays accepted even if its profile later changes. Pre-existing; acceptable at identity/provisioning granularity. Flagged in case revocation expectations are stronger.

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.

npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 3 commits June 21, 2026 10:55
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant