fix(cli): enrich template cardinality error with per-candidate presence and profile hints - #4825
Merged
Merged
Conversation
…oned-at hints When buzz channels create --template fails with a duplicate-instance error (persona has N > 1 live instances), the error listed bare pubkeys only — indistinguishable at a glance for agents whose kind:0 metadata looks identical (same name, same avatar). Add best-effort hint decoration to the error output: - presence status (online/offline) from kind:40902 - provisioned-at date (YYYY-MM-DD) from kind:0 created_at In the incident that surfaced this defect, the stale instances were precisely the offline ones, making presence the highest-signal field for deciding which to archive. Design constraints preserved: - apply_cardinality_rule stays pure: hints are fetched by the async caller (build_roster_resolution) and passed in, so the rule remains directly unit-testable without relay I/O. - Fail-open: if either lookup fails, the error prints with bare pubkeys rather than failing in a new way. Absent hint entries are silently omitted per candidate. - Zero-instance and single-instance paths are unchanged. - format_candidate is a pure helper, separately testable. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
- Rename provisioned_at -> profile_updated_at: kind:0 is replaceable
state (desktop republishes on rename/reconciliation), so created_at
reflects last profile update, not provisioning time. Output changes
from 'provisioned YYYY-MM-DD' to 'profile updated YYYY-MM-DD'.
- Make hint fetching operationally fail-open: happy path (no duplicate
live instances after archive filtering) performs zero hint queries.
When duplicates exist, the two relay lookups (presence + profile) run
concurrently via tokio::join! and the whole enrichment phase is bounded
by a 3-second timeout; on expiry the error prints promptly with bare
pubkeys. scan_managed_agents and fetch_archived_snapshot also run
concurrently now. Fixes the 'fire-and-forget' comment that was false.
- Extract build_hint_map as a sync production function taking raw
presence/profile event slices, so it is directly unit-testable without
a relay. fetch_candidate_hints becomes a thin async wrapper: query
concurrently, parse, delegate. Five boundary tests added:
- p-tag beats author for relay-signed presence events
- presence failure does not suppress profile hints
- profile failure does not suppress presence hints
- malformed entries are skipped without panic
- both failures yield empty map (bare pubkeys in error)
Mutation check: deleting p-tag selection fails 1 test; gutting
build_hint_map fails 3 tests.
- Reuse presence_subject from users.rs (pub(crate)) instead of
re-implementing the same p-tag/author fallback inline.
- Replace hand-rolled Gregorian arithmetic with chrono::DateTime
(already a direct buzz-cli dep); out-of-range timestamp omits the
hint rather than computing garbage.
- Fix kind-40902 comment: relay-synthesized on demand, not
parameterised-replaceable.
333/333 buzz-cli tests pass. fmt + clippy clean.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Extract assemble_roster_resolution<F, Fut> from build_roster_resolution containing duplicate detection, conditional fetch_hints call, and finalize_roster_resolution delegation. build_roster_resolution reduces to: gather slugs, tokio::join! scan + archive, delegate with the real fetch_candidate_hints closure. Four tokio::test cases against the production function pin both mutations: - duplicate pair -> fetcher invoked with exactly those pubkeys (mut a fails) - single instance -> fetcher never called / panic fires (mut b fails) - trusted archive archives one of a pair -> fetcher suppressed (mut b fails) - untrusted archive with pair -> fetcher called conservatively (mut a fails) Mutation (a) replace conditional fetch with HashMap::new(): 2 tests fail Mutation (b) remove emptiness gate / always fetch: 2 tests fail 337/337 buzz-cli tests pass. fmt + clippy clean. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…late-cardinality-hints Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
wesbillman
had a problem deploying
to
codex-review
August 26, 2026 23:25 — with
GitHub Actions
Error
…endently The duplicate-instance error omitted the `offline` label on the stale instance that operators most need flagged: the relay drops the Redis presence key when an identity goes offline, so the bulk snapshot never returns an event for it. Treat a successful, parseable presence response as a complete snapshot for the requested pubkeys — seed all of them `offline`, then overlay returned statuses. A failed, timed-out, or malformed response seeds nothing, so absence is never falsely inferred as offline. The shared `timeout(join!(..))` also discarded a completed lookup whenever its sibling hung past 3s. Bound each query independently and join the outcomes so a completed presence or profile result survives a hung sibling. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…late-cardinality-hints * origin/main: (145 commits) chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663) chore(deps): update dependency vitest to v4.1.11 (#6667) chore(deps): update dependency @tanstack/react-virtual to v3.14.10 (#6666) chore(deps): update ubuntu:24.04 docker digest to 33ceb71 (#6664) fix(projects): allow owners to delete agent projects (#6533) Fade expanded video controls on hover (#6926) fix(db): exclude kind:30179 ciphertext from brownfield FTS (#6822) fix(client): resurface hidden DMs from live activity (#6885) fix(desktop): keep the draft space when typing right after a mention pick (#6875) broker: define the agent-to-broker action contract (#6742) fix(desktop): keep project sheets independent from threads (#6901) Add gated security reviews (#6816) fix(desktop): accent-colored mention badges that count thread mentions (#6900) Add Buzz benchmark evaluation layers (#6823) fix(desktop): show edited head content in thread panel (#6887) fix(desktop-tooltip): increase surface contrast (#6897) Deduplicate ACP thread prompt context (#6706) Apply access policy when reusing channel agents (#6838) feat(sidebar): prioritize unread DMs in overflow navigation (#6842) feat(projects): add agent and CLI project-home support (#6590) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96
had a problem deploying
to
codex-review
August 27, 2026 16:19 — with
GitHub Actions
Error
… lookup failures
The offline-seeding boundary trusted any presence body that parsed as a JSON array, so `[{}]`, `[null]`, or an event with unreadable `content` was accepted as an authoritative snapshot and seeded every candidate `offline`. Now a snapshot is trusted only when every element is a well-formed presence event (readable subject + string content); any malformed element makes presence enrichment untrusted (seed nothing, keep the profile sibling's hints).
On the relay, `synthesize_presence` converted a `get_presence_bulk` error into an empty HTTP 200, so a Redis outage was indistinguishable from an authoritative all-offline snapshot. It now returns the lookup failure (and internal build/sign faults) as an error response instead of a fake-empty success.
Adds a production-wiring seam test that drives fetch_candidate_hints through a controlled query server where presence completes and profile hangs, plus malformed-element, relay-error, and Redis-connection-failure boundary tests.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
had a problem deploying
to
codex-review
August 27, 2026 16:52 — with
GitHub Actions
Error
The trust boundary accepted any array element with a non-empty subject and string content, so a vacuous object or a fully-shaped event for an unrequested subject counted as an authoritative snapshot and re-seeded every requested candidate offline. trusted_presence_snapshot now parses each element as a complete signed nostr::Event, requires kind KIND_PRESENCE_UPDATE, and requires its p-tag subject to be one of the requested pubkeys; any failure makes presence untrusted while the profile sibling survives. Narrow synthesize_presence to take (pubsub, relay_keypair) so its Redis-error seam is unit-testable without live infrastructure; add a CI-runnable relay test that a failing Redis lookup yields HTTP 500, not a fake-empty 200. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
had a problem deploying
to
codex-review
August 27, 2026 17:22 — with
GitHub Actions
Error
The trust predicate accepted an event when any p tag was requested (event.tags.public_keys().any(...)), but build_hint_map consumes the first p tag via presence_subject. A mixed-tag event like [["p","<unrequested>"],["p","<requested>"]] passed the gate, then overlaid the unrequested subject and left the requested candidate falsely seeded offline — the exact false-label class this boundary exists to prevent. Extract one canonical subject with sole_p_tag_subject, which reads p tags the same way the consumer does and requires exactly one; require that sole subject to be requested. Since a trusted event now carries exactly one p-tag subject, build_hint_map cannot reinterpret it differently. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
had a problem deploying
to
codex-review
August 27, 2026 17:40 — with
GitHub Actions
Error
wpfleger96
pushed a commit
that referenced
this pull request
Aug 27, 2026
…arer-auth * origin/main: fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
salman1993
added a commit
that referenced
this pull request
Aug 27, 2026
…cp-sessions * origin/main: test(db): use canonical channel roster fixtures (#6819) preserve channel description paragraph breaks (#6946) fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Fix Codex security review authorization (#6913) fix(db): disable heartbeat vacuum truncation (#6898) chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663) chore(deps): update dependency vitest to v4.1.11 (#6667) chore(deps): update dependency @tanstack/react-virtual to v3.14.10 (#6666) chore(deps): update ubuntu:24.04 docker digest to 33ceb71 (#6664) fix(projects): allow owners to delete agent projects (#6533) Fade expanded video controls on hover (#6926) fix(db): exclude kind:30179 ciphertext from brownfield FTS (#6822) fix(client): resurface hidden DMs from live activity (#6885) fix(desktop): keep the draft space when typing right after a mention pick (#6875) broker: define the agent-to-broker action contract (#6742) fix(desktop): keep project sheets independent from threads (#6901) Add gated security reviews (#6816) Signed-off-by: Salman Mohammed <smohammed@squareup.com>
wpfleger96
pushed a commit
that referenced
this pull request
Aug 27, 2026
…-history * origin/main: feat(desktop): implement 30178 team catalog backend (#5112) feat(model-capabilities): humanize Databricks UC model families (#6955) feat(agent): discover Databricks Unity Catalog models (#6918) test(db): use canonical channel roster fixtures (#6819) preserve channel description paragraph breaks (#6946) fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Fix Codex security review authorization (#6913) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
jrobotham-square
added a commit
that referenced
this pull request
Aug 28, 2026
…age-rw * origin/main: (21 commits) fix(desktop): resolve exact typed mentions on space (#6862) perf(desktop): restore project context during startup (#6939) fix(desktop): lift right auxiliary pane above shared header backdrop (#6966) fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962) feat(desktop): implement 30178 team catalog backend (#5112) feat(model-capabilities): humanize Databricks UC model families (#6955) feat(agent): discover Databricks Unity Catalog models (#6918) test(db): use canonical channel roster fixtures (#6819) preserve channel description paragraph breaks (#6946) fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Fix Codex security review authorization (#6913) fix(db): disable heartbeat vacuum truncation (#6898) chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663) chore(deps): update dependency vitest to v4.1.11 (#6667) chore(deps): update dependency @tanstack/react-virtual to v3.14.10 (#6666) chore(deps): update ubuntu:24.04 docker digest to 33ceb71 (#6664) fix(projects): allow owners to delete agent projects (#6533) Fade expanded video controls on hover (#6926) fix(db): exclude kind:30179 ciphertext from brownfield FTS (#6822) fix(client): resurface hidden DMs from live activity (#6885) ... Signed-off-by: Joel Robotham <jrobotham@squareup.com>
wpfleger96
pushed a commit
that referenced
this pull request
Aug 28, 2026
…agent-edit * origin/main: feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776) fix(desktop): resolve exact typed mentions on space (#6862) perf(desktop): restore project context during startup (#6939) fix(desktop): lift right auxiliary pane above shared header backdrop (#6966) fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962) feat(desktop): implement 30178 team catalog backend (#5112) feat(model-capabilities): humanize Databricks UC model families (#6955) feat(agent): discover Databricks Unity Catalog models (#6918) test(db): use canonical channel roster fixtures (#6819) preserve channel description paragraph breaks (#6946) fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Fix Codex security review authorization (#6913) fix(db): disable heartbeat vacuum truncation (#6898) chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96
pushed a commit
that referenced
this pull request
Aug 28, 2026
…etection * origin/main: (24 commits) feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776) fix(desktop): resolve exact typed mentions on space (#6862) perf(desktop): restore project context during startup (#6939) fix(desktop): lift right auxiliary pane above shared header backdrop (#6966) fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962) feat(desktop): implement 30178 team catalog backend (#5112) feat(model-capabilities): humanize Databricks UC model families (#6955) feat(agent): discover Databricks Unity Catalog models (#6918) test(db): use canonical channel roster fixtures (#6819) preserve channel description paragraph breaks (#6946) fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Fix Codex security review authorization (#6913) fix(db): disable heartbeat vacuum truncation (#6898) chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663) chore(deps): update dependency vitest to v4.1.11 (#6667) chore(deps): update dependency @tanstack/react-virtual to v3.14.10 (#6666) chore(deps): update ubuntu:24.04 docker digest to 33ceb71 (#6664) fix(projects): allow owners to delete agent projects (#6533) Fade expanded video controls on hover (#6926) fix(db): exclude kind:30179 ciphertext from brownfield FTS (#6822) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
TheSentinel454
added a commit
that referenced
this pull request
Aug 28, 2026
…edia-layout-migration * commit 'e76c81968b65b0755b83efdd59dc3375c59ddf40': (159 commits) refactor(db): split channel membership store (#6782) feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776) fix(desktop): resolve exact typed mentions on space (#6862) perf(desktop): restore project context during startup (#6939) fix(desktop): lift right auxiliary pane above shared header backdrop (#6966) fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962) feat(desktop): implement 30178 team catalog backend (#5112) feat(model-capabilities): humanize Databricks UC model families (#6955) feat(agent): discover Databricks Unity Catalog models (#6918) test(db): use canonical channel roster fixtures (#6819) preserve channel description paragraph breaks (#6946) fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Fix Codex security review authorization (#6913) fix(db): disable heartbeat vacuum truncation (#6898) chore(deps): update rui314/setup-mold digest to 7e4f20a (#6663) chore(deps): update dependency vitest to v4.1.11 (#6667) chore(deps): update dependency @tanstack/react-virtual to v3.14.10 (#6666) chore(deps): update ubuntu:24.04 docker digest to 33ceb71 (#6664) fix(projects): allow owners to delete agent projects (#6533) Fade expanded video controls on hover (#6926) ... # Conflicts: # crates/buzz-deletion/src/lib.rs # crates/buzz-media/Cargo.toml # crates/buzz-media/src/lib.rs # crates/buzz-media/src/storage.rs
wpfleger96
pushed a commit
that referenced
this pull request
Aug 28, 2026
…c-agent-commit-identity * origin/main: feat(desktop): add team sharing to community catalog (#3995) Refresh mobile utility surfaces and theme picker (#6944) fix(desktop): complete project empty and context states (#6980) Fix mobile jump-to-latest flicker (#6807) refactor(relay): NIP-98 admin auth with Operator/Moderator roles and NIP-11 discovery (#3777) refactor(db): split channel membership store (#6782) feat(auth): add NIP-FI canonical assertion verifier and contracts (#6776) fix(desktop): resolve exact typed mentions on space (#6862) perf(desktop): restore project context during startup (#6939) fix(desktop): lift right auxiliary pane above shared header backdrop (#6966) fix(ci): bump Codex CLI to 0.150.1 to unhang security review jobs (#6962) feat(desktop): implement 30178 team catalog backend (#5112) feat(model-capabilities): humanize Databricks UC model families (#6955) feat(agent): discover Databricks Unity Catalog models (#6918) test(db): use canonical channel roster fixtures (#6819) preserve channel description paragraph breaks (#6946) fix(cli): enrich template cardinality error with per-candidate presence and profile hints (#4825) Fix Codex security review authorization (#6913) fix(db): disable heartbeat vacuum truncation (#6898) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.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.
Problem
buzz channels create --templatefails with a bare-pubkey duplicate-instance error when a persona has more than one live instance. When duplicate instances share the same display name and avatar, the error gives the operator no signal to identify which instance is stale.What changed
crates/buzz-cli/src/commands/channels.rs(boundary) + one visibility line incrates/buzz-cli/src/commands/users.rs(fn presence_subject→pub(crate)), plus a relay behavior change incrates/buzz-relay/src/api/bridge.rsand a supporting test incrates/buzz-pubsub/src/presence.rs.Relay: presence-lookup failure is now surfaced as a failure
synthesize_presencepreviously collapsed a Redisget_presence_bulkerror intounwrap_or_default()→ HTTP 200[], making a backend outage indistinguishable from an authoritative all-offline snapshot. It now returnsOption<Result<Vec<Value>, (StatusCode, Json<Value>)>>:Nonemeans the request is not a presence query (fall through),Some(Ok)is a real snapshot (an empty vec is an authoritative all-offline result), andSome(Err)is a backend failure surfaced as a non-2xx response. The event tag-parse and sign paths were converted from.ok()?fall-throughs to explicitSome(Err(...))for the same reason — a silent fall-through would have reintroduced the fake-empty-success anti-pattern. The function takespubsubandrelay_keypairdirectly rather than the wholeAppState, which keeps the Redis-error seam unit-testable without live infrastructure; its single production caller was updated accordingly, and no other caller consumessynthesize_presence.Hint fetch architecture
assemble_roster_resolution<F, Fut>— post-fetch stage extracted frombuild_roster_resolutionas an injectable-fetcher async generic. It contains: archive-filter-based duplicate detection, the conditionalfetch_hintscall, andfinalize_roster_resolutiondelegation. Accepting the fetcher as a closure makes the wiring directly testable without a relay.build_roster_resolutionreduces to: gather slugs →tokio::join!(scan, archive)→ delegate with the realfetch_candidate_hintsclosure.Zero hint queries on the happy path:
assemble_roster_resolutiononly invokes the fetcher when duplicate live instances exist after archive filtering. Untrusted archive snapshot (Err) conservatively treats all found instances as live.When duplicates exist,
fetch_candidate_hintsruns the presence (kind:40902) and profile (kind:0) queries concurrently, bounding each independently throughjoin_bounded_queries: a per-query 3-second timeout maps toErr, so a lookup that completes is never discarded because its sibling hung. On total failure or timeout, bare pubkeys are printed promptly.Trusted-snapshot boundary —
hints_from_resultsThe relay drops the Redis presence key when an identity goes offline, so the bulk presence snapshot never returns an event for an offline instance — precisely the stale duplicate an operator needs flagged.
hints_from_resultstherefore treats a trusted presence snapshot as a complete snapshot for the requested pubkeys: it seeds all of themoffline, then overlays returned statuses.A response is trusted only when
trusted_presence_snapshotvalidates every array element as the actual snapshot contract: each must parse as a complete signednostr::Eventof the presence-update kind carrying exactly oneptag whose subject is one of the requested pubkeys. Requiring the solep-tag subject — the exact valuebuild_hint_maplater reads viapresence_subject— is what stops a mixed-tag event ([["p","<unrequested>"],["p","<requested>"]]) from passing the gate yet overlaying a different subject downstream. Any element that is not such an event — a vacuous object like{"pubkey":"…","content":"online"},[{}],[null], an event of the wrong kind, one for an unrequested subject, or one carrying more than oneptag — makes presence enrichment untrusted: nothing is seededoffline, so absence is never falsely inferred as offline. A failed, timed-out, or non-array presence response is likewise untrusted. In every untrusted case the successful profile sibling still contributes its hints.Response-to-map conversion —
build_hint_mapSync production function taking the offline seed plus raw presence/profile event slices. Relay-signed presence events carry the agent pubkey in the
ptag (not the event author);build_hint_mapcallspresence_subjectfromusers.rs(nowpub(crate)) to resolve the correct key.Hint display —
format_candidateAppends
[online/offline, profile updated YYYY-MM-DD](or subset) to each candidate pubkey in the error.profile_updated_atnames the field correctly: kind:0 is replaceable state that desktop republishes on rename and profile reconciliation, socreated_atreflects the last profile update, not provisioning time. Date formatted viachrono::DateTime::from_timestamp; out-of-range timestamps omit the date. Missing entries fall back to bare pubkeys.Cardinality rule
apply_cardinality_ruleremains pure and relay-free; zero/one/many semantics unchanged.