feat(buzz-acp): per-thread dispatch affinity — steer within a thread, parallelize new threads in one channel
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Motivation
An owner-only `buzz-acp` assistant in a busy channel can't handle two threads at once. Dispatch affinity is **per-channel, not per-thread**: all traffic in a channel funnels to one agent/session, so a second question aborts (under `steer`) or gets batched into (under `queue`) the first thread's in-progress work.
Controlled test — `meh=steer`, `agents=3`, a single channel, a same-thread reply plus 3 new top-level threads posted ~4s apart, `RUST_LOG=buzz_acp=debug`. From the log:
- All 5 events dispatched to `agent=0`; agents 1 & 2 were never used. `affinity_hit=false` on every event — **including the same-thread reply**.
- Under `steer`, **every** new event cancelled the in-flight turn — a new thread *and* a same-thread reply alike:
`control signal sent to in-flight task … mode=Steer` → `turn cancelled for channel …`.
- `non-cancelling steer enabled (try-and-tolerate)` fell back to `cancel+merge` all 4 times (`goose-native steer: no active_run_id at write time … falling back to cancel+merge`), i.e. a hard cancel for turns that haven't started streaming yet.
Harness startup line (identifiers redacted):
```text
subscribe=Config dedup=Queue meh=Steer agents=3 idle_timeout=900s max_turn=7200s
context_limit=12 max_turns_per_session=0 respond_to=owner-only
agent=@agentclientprotocol/claude-agent-acp 0.50.0
relay=wss://buzz.block.builderlab.xyz/
```
The 3-agent pool only parallelizes across **different channels**; two threads in one channel always serialize onto one agent.
## Proposed solution
Key the in-flight / affinity check on **thread root** (the reply-to root), not the channel. Either or both of:
1. **Per-thread affinity in the dispatcher** — a new thread claims a free pool agent; a same-thread reply steers *its own* thread's in-flight turn.
2. **A new `multiple-event-handling` mode (e.g. `steer-thread`)** — steer only when the incoming event shares the in-flight turn's thread root; otherwise dispatch to a free pool agent, or queue if the pool is exhausted.
## Alternatives considered
- **`meh=queue`** — never pre-empts, but per #3176 it batches independent events into a single ACP turn (loses one-event → one-reply identity) and offers no same-thread steering.
- **`meh=steer`** — the opposite: cancels everything on every new event (see also #3042 unbounded cancelled-batch growth, and #3282 idle thread-reply swallow).
- **One channel per workstream** — works today precisely because the pool is per-channel, but it's a workaround, not per-thread concurrency within a channel.
## Additional context
This is distinct from, but would relieve the root tension behind, these open issues: #3176, #3282, #3042 (all are bugs in the current per-channel steer/queue paths; this asks for the per-thread dispatch mode underneath them).
Repro uses a throwaway agent identity in an ephemeral private channel — happy to share the full debug log and the repro script.
Contributor guide
Research direction
Start at the dispatcher and the multiple-event-handling configuration path, then compare the per-channel affinity and in-flight checks with the controlled debug log described here. Define thread-root ownership so same-thread replies steer their own turn while new threads use free agents or queue when exhausted; validate the one-event-to-one-reply behavior against the listed steer and queue regressions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend, distributed-systems
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100