fix(acp): follow threads the agent participates in (#2270) - #2375
fix(acp): follow threads the agent participates in (#2270)#2375brocoppler wants to merge 5 commits into
Conversation
8173991 to
930c0ef
Compare
|
Blocking evidence against current head Control-flow evidenceThe current head does these three things in sequence:
Once sibling agents A and B have both participated in one thread, an unmentioned reply from B can therefore wake A. A's reply can wake B through the same rule. That is the agent-to-agent loop #2270 explicitly says must remain impossible. This is not covered by the new tests. They prove that a generic unmentioned event in a followed root is admitted, but do not exercise author identity or two participating agents. Required correctionPlease keep explicit agent mentions working, but reject agent-authored, unmentioned events that are admitted only because their root is followed. If the proposed Regression coverage should include:
The PR description also states that this has not been exercised against a live relay. Please add the end-to-end relay case before treating #2270/#2332 as resolved: mention agent → agent replies → human sends an untagged thread reply → harness receives exactly one new turn. Separate timing evidence for successful explicit mentions is now tracked in #2386; this comment is about correctness and loop safety, not model latency. |
Review of block#2375 (BradGroux) identified an agent-to-agent loop the initial thread-following change permitted: ignore_self blocks self-loops and the author gate blocks strangers, but same-owner sibling agents pass the gate by design — so two siblings participating in one thread could wake each other with unmentioned replies indefinitely, each turn refreshing the follow TTL. Followed-thread admissions are now author-aware: - match_event reports the admission basis (admitted_via_follow) so the caller can distinguish follow-bypass admissions from mention matches. - A follow-only admission from an agent author is suppressed under the new --follow-thread-authors policy (BUZZ_ACP_FOLLOW_THREAD_AUTHORS): humans (default) admits human authors only; all opts into agent authors and accepts the auto-continuation risk. Explicit mentions are never affected, and suppression happens before the TTL refresh so agent chatter cannot keep a thread alive. - Agenthood uses the existing NIP-OA heuristic (pool::profile_event_is_agent, now pub(crate)) via a cached kind:0 lookup that runs only on the rare follow-admitted path; unknown identities classify as human (fail open, matching turn_is_human_facing). The security gate remains author_allowed. Regression coverage per review: human unmentioned reply admitted; agent-authored unmentioned reply dropped (covers siblings and external agents under respond-to anyone); mentioned agent-authored replies unaffected; all-policy opt-in; and the two-agent auto-continuation scenario cannot start from either side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Broc Oppler <brocoppler@gmail.com>
|
The loop was real:
Regression coverage per your list: human unmentioned reply in a followed root admitted; agent-authored unmentioned reply dropped (siblings and external-under- Live-relay run (local relay, this branch, Claude Code behind
Observed turn latency (~15–30s) matches #2386. The PR description's "not yet exercised against a live relay" line is updated. Can add these scenarios as a |
6318ba4 to
a72b450
Compare
Review of block#2375 (BradGroux) identified an agent-to-agent loop the initial thread-following change permitted: ignore_self blocks self-loops and the author gate blocks strangers, but same-owner sibling agents pass the gate by design — so two siblings participating in one thread could wake each other with unmentioned replies indefinitely, each turn refreshing the follow TTL. Followed-thread admissions are now author-aware: - match_event reports the admission basis (admitted_via_follow) so the caller can distinguish follow-bypass admissions from mention matches. - A follow-only admission from an agent author is suppressed under the new --follow-thread-authors policy (BUZZ_ACP_FOLLOW_THREAD_AUTHORS): humans (default) admits human authors only; all opts into agent authors and accepts the auto-continuation risk. Explicit mentions are never affected, and suppression happens before the TTL refresh so agent chatter cannot keep a thread alive. - Agenthood uses the existing NIP-OA heuristic (pool::profile_event_is_agent, now pub(crate)) via a cached kind:0 lookup that runs only on the rare follow-admitted path; unknown identities classify as human (fail open, matching turn_is_human_facing). The security gate remains author_allowed. Regression coverage per review: human unmentioned reply admitted; agent-authored unmentioned reply dropped (covers siblings and external agents under respond-to anyone); mentioned agent-authored replies unaffected; all-policy opt-in; and the two-agent auto-continuation scenario cannot start from either side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Broc Oppler <brocoppler@gmail.com>
Drives the real buzz-acp binary with a minimal stub ACP agent (stub-acp-agent, NDJSON JSON-RPC over stdio) against a live relay and asserts turn dispatch for the four cases from the block#2375 review: explicit mention fires a turn and follows the thread; an untagged human reply in the followed thread fires a second turn; an untagged agent-authored reply (NIP-OA-tagged kind:0, respond-to=anyone so the followed-thread author policy is the only gate) is suppressed with the suppression log line asserted; an untagged top-level message stays gated. The stub logs each session/prompt to a file and never contacts the relay: participation is recorded by the harness at dispatch, so turn counts alone prove admission/suppression. The agent reply path is out-of-band by design (block#2459 tracks its observability). #[ignore]-gated like the rest of the e2e suite; requires a running relay and a built buzz-acp (BUZZ_ACP_BIN override supported). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Broc Oppler <brocoppler@gmail.com>
|
Integration test added in b9700ca: |
|
Status roll-up since head b9700ca:
@wpfleger96 @tlongwell-block — needs the first-contributor CI approval click to run in-repo. Can split the e2e harness (stub agent + test-client wiring) into its own PR if that makes review easier. |
A mention-gated agent (SubscribeMode::Mentions, the default) only ever received events carrying its #p tag, at two independent gates: the relay REQ filter and match_event's re-check. Once the agent replied in a thread, untagged follow-ups in that same thread were never delivered — the agent went deaf mid-conversation unless re-mentioned on every message. This records thread participation at dispatch time and opens both gates for followed threads only: - thread_follow.rs: per-channel set of participated NIP-10 thread roots with a sliding inactivity TTL (default 24h, --thread-follow-ttl-secs), a per-channel cap (64, stalest evicted), and dirty-channel tracking. Participation is recorded when a batch is dispatched — the batch's thread root, or the triggering event id when the reply starts a new thread (mirroring resolve_reply_anchor). - relay.rs: REQ construction extracted into build_req_filters(), which emits a second filter clause (#h + #e limited to followed roots, same kinds/since) when the base clause is mention-gated. The mention gate stays intact for everything else; re-issued REQs replay from last_seen minus skew, so replies landing during the update window are recovered. - filter.rs: match_event takes the followed-root set and admits an unmentioned event whose thread root is followed. Following relaxes only the mention gate — channel scope, kinds, and expression filters still apply. - Followed threads expire after the TTL and the REQ clause is narrowed again; channel unsubscribe clears its follow state. --no-follow-threads restores the previous strict-mention behavior. Closes block#2270. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Broc Oppler <brocoppler@gmail.com>
Review of block#2375 (BradGroux) identified an agent-to-agent loop the initial thread-following change permitted: ignore_self blocks self-loops and the author gate blocks strangers, but same-owner sibling agents pass the gate by design — so two siblings participating in one thread could wake each other with unmentioned replies indefinitely, each turn refreshing the follow TTL. Followed-thread admissions are now author-aware: - match_event reports the admission basis (admitted_via_follow) so the caller can distinguish follow-bypass admissions from mention matches. - A follow-only admission from an agent author is suppressed under the new --follow-thread-authors policy (BUZZ_ACP_FOLLOW_THREAD_AUTHORS): humans (default) admits human authors only; all opts into agent authors and accepts the auto-continuation risk. Explicit mentions are never affected, and suppression happens before the TTL refresh so agent chatter cannot keep a thread alive. - Agenthood uses the existing NIP-OA heuristic (pool::profile_event_is_agent, now pub(crate)) via a cached kind:0 lookup that runs only on the rare follow-admitted path; unknown identities classify as human (fail open, matching turn_is_human_facing). The security gate remains author_allowed. Regression coverage per review: human unmentioned reply admitted; agent-authored unmentioned reply dropped (covers siblings and external agents under respond-to anyone); mentioned agent-authored replies unaffected; all-policy opt-in; and the two-agent auto-continuation scenario cannot start from either side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Broc Oppler <brocoppler@gmail.com>
Drives the real buzz-acp binary with a minimal stub ACP agent (stub-acp-agent, NDJSON JSON-RPC over stdio) against a live relay and asserts turn dispatch for the four cases from the block#2375 review: explicit mention fires a turn and follows the thread; an untagged human reply in the followed thread fires a second turn; an untagged agent-authored reply (NIP-OA-tagged kind:0, respond-to=anyone so the followed-thread author policy is the only gate) is suppressed with the suppression log line asserted; an untagged top-level message stays gated. The stub logs each session/prompt to a file and never contacts the relay: participation is recorded by the harness at dispatch, so turn counts alone prove admission/suppression. The agent reply path is out-of-band by design (block#2459 tracks its observability). #[ignore]-gated like the rest of the e2e suite; requires a running relay and a built buzz-acp (BUZZ_ACP_BIN override supported). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Broc Oppler <brocoppler@gmail.com>
b9700ca to
868b3bf
Compare
|
Rebased onto today's main (head now 868b3bf, post-#2974 runtime refactor) — clean, no conflicts; unit suite (643 passed) + the Description updated to match the new PR guide from #3140: verification evidence is now inline, including the two independent user confirmations on #2270 (one from the owner-repro side, one agent-perspective). Review-ready as it stands; happy to split or adjust scope if a different shape merges easier. |
|
Thanks for the careful revision, the rebase, and the live-relay work. I re-reviewed current head Several parts of the response are valid and address the original finding:
Two correctness blockers remain.
There are two contract/documentation gaps as well:
I compiled the new E2E target successfully. I could not independently execute it because this review environment had no local relay or Docker runtime, so the source-level gaps above matter: the published test currently cannot establish the claims it is being used to support. The original loop finding was valid, and this revision materially improves the PR, but I do not think it is ready for approval yet. A fail-closed/positively-proven-human boundary, exact per-event turn assertions, and the config contract/docs cleanup would make the re-review straightforward. |
Address the remaining review blockers for thread following by verifying agent attestations, requiring positive human classification, documenting the controls, and strengthening the live-relay regression test. Co-authored-by: Paul Armbruster <paul@21million.ad> Signed-off-by: Paul Armbruster <paul@21million.ad>
|
I prepared a focused follow-up patch for the remaining review blockers: https://github.com/brocoppler/buzz/pull/1\n\nIt makes the humans-only boundary fail closed, verifies NIP-OA attestations cryptographically, falls back to NIP-29 member roles, strengthens the live-relay test to cover a visible agent reply followed by an unmentioned human reply with exact turn counts, and documents the three settings. The buzz-acp suite (637 + 9 tests), clippy, formatting, and E2E compilation pass. The live relay run is still pending because local Docker image pulls failed with an unexpected EOF. |
The fail-closed classifier resolved humans through two uncached relay round-trips on every follow-admitted event — the common case this feature serves. Cache proven classes, keeping the NIP-OA attestation global (identity-level) and the member-role result channel-scoped, since an identity may hold different roles per channel. Unknown is never cached, so a transient lookup failure cannot pin an author as suppressed. Extend the live-relay test with a non-owner human member, which is the case that exercises member-role resolution rather than the configured- owner shortcut, and anchor the trailing no-new-turn assertions to the observed count. Signed-off-by: Broc Oppler <brocoppler@gmail.com>
|
Both blockers are addressed at head 1. The guard now fails closed. 2. The test proves exactly one turn. I added a case beyond the patch: a non-owner human member. Every human in the original test was the configured owner, which short-circuits at the top of the classifier — so a wrong assumption about the member-role tag layout would have suppressed every real human member while the suite stayed green. The added case forces resolution through kind:39002. (I verified the layout independently: the relay writes Fixed in the patch: it resolved humans through two uncached relay round-trips on every follow-admitted event — the common case this feature exists to serve, on the dispatch path, at up to 2s each. Proven classes are now cached, with the NIP-OA result global (identity-level, immutable) and the member-role result channel-scoped, since one identity can be a plain member in one channel and a 3. Config docs. The three settings are documented in the buzz-acp README (flag / env / default / description) and 4. Rule-level contract — narrowed explicitly, per your first option, and written into the PR description rather than left implicit. This PR ships global CLI/env settings and does not claim per-rule Head |
Summary
A mention-gated agent (
SubscribeMode::Mentions, the default) goes deaf in threads it has joined: once it replies, untagged follow-ups like "yes, go ahead" are never seen unless the human re-tags the agent on every message. The gate exists at two levels — the relay REQ's#pclause never delivers the reply, andmatch_event's mention re-check would drop it anyway.Fix: participation opens a thread; inactivity closes it.
thread_follow.rs(new): per-channel set of followed NIP-10 thread roots with a sliding inactivity TTL (default 24h,--thread-follow-ttl-secs/BUZZ_ACP_THREAD_FOLLOW_TTL_SECS) and a hard per-channel cap (64, least-recently-active evicted). Participation is recorded at dispatch time: the batch's thread root, or the triggering event id when the agent's reply is about to start a new thread (mirrorsresolve_reply_anchor— this is the exact repro case in [Bug] buzz-acp agents go deaf in threads they've joined unless re-mentioned. #2270). Admitted followed-thread events refresh the TTL, so live conversations keep sliding.build_req_filters(), which emits a second filter clause — same#h/kinds/since, plus#elimited to followed roots — only when the base clause is mention-gated. The mention gate stays intact for everything else; no channel firehose. Subscription updates reuse the existing per-channel sub-id replacement path and replay fromlast_seenminus skew, so replies landing during the update window are recovered. Resubscribes fire only on thread join/expiry, not per message, staying inside the pacing envelope from fix(acp): honor relay rate limits and pace resubscribes on bad links #2199/fix(acp): pace relay observer frames (6/s + 90/min, zero burst) #2217.match_eventtakes the followed set and admits an unmentioned event only when its thread root is followed. Channel scope, kinds, and evalexpr filters still apply — following relaxes only the mention check.--no-follow-threads(BUZZ_ACP_NO_FOLLOW_THREADS) restores the previous strict-mention behavior. State is in-memory, matching the harness's existing recovery semantics (after restart, a fresh mention re-joins a thread).Related issue
Fixes #2270 (independently re-reported as #2332). The patch is confirmed by two affected users on the issue: oveddan (owner-replied-in-thread repro) and dmnyc (agent-perspective confirmation).
Scope narrowing (explicit). #2270 sketches per-rule
follow_participating_threads/follow_thread_authors; this PR implements them as global CLI/env settings instead, and does not claim the rule-level contract. Reason: the followed-thread REQ clause is channel-scoped, so per-rule control needs union-at-REQ plus per-rule enforcement inmatch_event, and two rules disagreeing on the same channel have no coherent subscription answer. That is a subscription-design change rather than part of this fix. It is implementable on top of this (the REQ clause is a delivery optimization; correctness already lives inmatch_event) — happy to do it as a follow-up if the rule-level contract is wanted.Testing
crates/buzz-test-client/tests/e2e_acp_thread_follow.rs): drives the realbuzz-acpbinary with a stub ACP agent against a live relay — asserts a mention opens the thread, an untagged in-thread follow-up is admitted, untagged replies in unfollowed threads stay suppressed, and agent-authored unmentioned replies stay suppressed under--respond-to anyone.match_eventfollowed-thread admission (admits followed, ignores unfollowed and top-level, relaxes only the mention gate), andbuild_req_filtersclause construction (base-only, roots-ignored-without-mention-gate, two-clause shape).cargo test -p buzz-acp: 643 passed, 0 failed.cargo clippy -p buzz-acp --all-targetsandcargo fmtclean.claude-code-acp, live local relay): mention answered in-thread; untagged human follow-up answered (~15s,ptags empty); untagged replies in unfollowed threads silent; agent-authored unmentioned replies suppressed under bothowner-onlyand--respond-to anyone(transcripts in the review thread).2af74400: 647 unit tests pass (638 + 9),cargo clippy -p buzz-acp --all-targets -- -D warningsclean,cargo fmtclean, and the live-relay e2e passes against a local relay (~31s).botrole proves an agent. Missing profiles, unresolved membership, malformed events, and relay timeouts stayUnknownand still require an explicit mention under the defaulthumanspolicy.>=), publishes a visible agent reply so the human follow-up replies to a real message, and covers a non-owner human member — the case that exercises member-role resolution rather than the configured-owner shortcut..env.example.Follow-up candidates (out of scope here)
🤖 Generated with Claude Code