mirror of
https://github.com/block/buzz.git
synced 2026-11-01 15:01:12 +00:00
The first cut of dead-turn detection asked "did this turn produce any output at all", which is both too broad and too narrow. Too broad: `PromptSource::Channel` covers every matched batch, not just mentions. Under `--subscribe all` or a `require_mention: false` rule an agent watches traffic nobody addressed to it, and base_prompt.md tells it to stay silent when it has nothing to add. Retrying that turned correct restraint into ten retries and a user-visible failure notice. `MatchedRule` now carries `requires_response` — the rule required a mention, or the event p-tags the agent — and it rides through `QueuedEvent`, `BatchEvent` and `FlushBatch` so classification can ask whether anyone was actually owed an answer. Too narrow: an event injected mid-turn by a native steer is not in the batch, and the ack used to drop it outright. If the turn then went silent, that mention was gone with no retry — the same disappearance this change exists to close, one layer down. A `Success` ack now moves the event into a delivered-steer ledger keyed by the turn's output epoch, and the turn's terminal classification settles it: output after the delivery retires it, silence releases it to the queue front for an ordinary, fully-budgeted retry. The per-turn boolean became that monotonic epoch counter because a flag cannot tell "spoke, then went silent on the new message" from "answered the steer". Expiry and panic recovery settle the ledger too, so a delivered event can never outlive its turn. Two smaller corrections. The ACP schema lets the initial `tool_call` carry `status: "completed"`, so a connector that sends no follow-up `tool_call_update` was being read as dead after doing real work; one shared predicate now covers both events. And the control-signal race branch, which synthesizes its own `EndTurn`, routes through the same classifier rather than relying on a polling invariant that lives in another function — the invariant holds today, but one inserted `.await` would silently turn that branch into a bypass. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>