fix(composer): polish automatic mentions - #6956
Conversation
🔐 Codex Security Review
|
jedwards27
left a comment
There was a problem hiding this comment.
Reviewing exact head 34d208b47d64a9816f88e10a46bcfd479e917d75 against base 69096c9a8db726eebd83422cdc5e6c72ed9d4f90, I found three author-actionable blockers.
-
[P2] Persisting a draft can delete an authored duplicate leading mention.
stripImplicitAgentMentionsrepeatedly removes every matching leading display-name token (desktop/src/features/messages/lib/stripImplicitAgentMentions.ts:15-22), but the persistence call sites provide only the locked agents' names, not the provenance or boundary of the synthesized prefix (desktop/src/features/messages/MessageComposer.tsx:131-133,203-207,461-465;desktop/src/features/messages/hooks/useDraftPersistSnapshot.ts:129-135,206-216). With one automatic@Morgaritaprefix and authored content@Morgarita authored duplicate, the combined editor value@Morgarita @Morgarita authored duplicatepersists asauthored duplicate; the user's authored mention is silently lost. Existing unit coverage handles wholly synthetic prefixes and non-leading mentions, but not this identical authored-leading case (desktop/src/features/messages/lib/stripImplicitAgentMentions.test.mjs:6-39).Author action: strip exactly the editor-inserted automatic prefix using explicit provenance/an exact captured prefix boundary, and add a regression covering an automatic prefix followed by an authored identical mention through channel/thread switch and restore.
-
[P2] Restoring an automatic mention concatenates it with the authored draft text. The PR-authored E2E
implicit automatic mentions stay out of persisted draftsreproducibly restores@Morgaritadraft textinstead of@Morgarita draft textafter adding the automatic mention, typingdraft text, opening a thread, and returning. The mention remains highlighted, so this is a user-visible draft corruption at the reinsertion/persistence boundary rather than an assertion-only failure.Author action: preserve exactly one separator when restoring an implicit addressed mention before authored text, retain valid mention identity/highlighting and caret behavior, and make the existing
persistent-agent-audience.spec.tscase pass. -
[P2] The required Desktop smoke gate is red on the changed mention-identity journey. In run
33110679208, job98696932534,desktop/tests/e2e/mentions.spec.ts:325failed all three attempts: selecting managed agentcarlsendslocal, butreadOutgoingMentionPubkeysis[]rather than[aaaaaaaa…]at lines 430-432. The aggregateDesktopgate is consequently red. This exercises exact-pubkey selection/signing behavior changed by this PR.Author action: fix the lost managed-agent recipient—or, if the fixture/oracle is wrong, correct it with a demonstrated causal explanation—and make
Desktop Smoke E2E (2)plus aggregateDesktopgreen at the reviewed head.
Validation at this immutable head: full unit suite 5,731/5,731 passed; pnpm typecheck, pnpm check, pnpm build, and git diff --check passed; targeted persistent-agent-audience.spec.ts was 25 passed / 1 failed on blocker 2. DCO, Desktop Core, macOS build, release candidate, and relay-backed integration gates are green. Native GUI/manual observation was not performed; that is a reviewer-owned confidence gap, not requested author work.
34d208b to
4a93533
Compare
|
AI-generated update: the three blocking paths are addressed at
Validation on the cleared tree: changed unit lanes 62/62, targeted draft pair 20/20 across ten consecutive paired runs, settings synchronization 4/4, modified desktop E2E selection 102/102, typecheck clean, and |
4a93533 to
2ce0137
Compare
|
@buzz-security-review 2ce0137 |
|
@jedwards27 AI-generated current-head update: the branch is now rebased onto The rebase integrated main's exact typed-mention-on-Space behavior. That behavior gives the authored duplicate its own valid mention identity after restoration, so the E2E now expects two highlighted mentions while continuing to assert the exact persisted authored duplicate content. Full desktop units pass 5,762/5,762, and the focused settings plus draft-restoration journeys pass 3/3. There are no unresolved inline threads. Please re-review the current head when ready. |
jedwards27
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Reviewed: b49675894b39f87215e4dfc8c1ad4a3c28c6097e..2ce0137f6f6988ea0621c201ab696f825e27ed42
Risk: high — this changes automatic mention identity, draft persistence, restored editor state, and exact message recipients.
Blocking finding
[P2] Preserve the inserted automatic-prefix provenance across profile/name refreshes
MessageComposer recomputes and overwrites the draft's supposedly captured prefix from the current lockedAgents display names on every render (desktop/src/features/messages/ui/MessageComposer.tsx:473-480). Those names are themselves refreshed from live profile data (desktop/src/features/messages/ui/useAgentAddressLockPicker.ts:103-125). Restore then checks the current text only for the new name and prepends it when absent (useAgentAddressLockPicker.ts:372-424), while persistence strips only the newly recomputed prefix (useDraftPersistSnapshot.ts:129-135; stripImplicitAgentMentions.ts:6-14).
That breaks the PR's persistence invariant under a normal async identity transition:
- The automatic recipient is inserted as
@OldNameand the user types a draft. - The same pubkey resolves to
NewNameafter a profile/relay refresh. - Restore considers
@NewNamemissing and prepends it. - Persistence strips
@NewNamebut leaves the synthetic@OldNameas authored content. - A later restore can show both recipient-looking prefixes.
The new stable-name duplicate test (desktop/tests/e2e/persistent-agent-audience.spec.ts:1103-1138) does not exercise this transition, and the nearest hook test covers the name cache becoming null rather than one non-null name changing to another (useAgentAddressLockPicker.test.mjs:792-824).
Author action: retain the exact automatic prefix (or identity-bound inserted range) at insertion/restore time instead of overwriting its provenance from later profile renders. Add a regression that changes one locked pubkey from a non-null old display name to a different non-null new name while authored content exists, crosses a persistence/remount boundary, and asserts: no duplicate visible prefix, no synthetic prefix in stored draft content, and the exact pubkey remains the recipient.
What is fixed
The prior stable-name failures are causally addressed: exact-prefix stripping preserves an identical authored mention; separator/caret restoration now has focused coverage; and the outgoing-recipient regression exercises typed content and asserts the exact managed/relay pubkey. The formerly failing smoke shard is green.
Validation at exact head
- PASS: focused prefix/address-lock tests, 19/19.
- PASS:
pnpm typecheck && pnpm check && pnpm build. - PASS: fresh
pnpm build:e2eplus four focused former-blocker Playwright journeys, 4/4. - PASS:
git diff --check b49675894b39f87215e4dfc8c1ad4a3c28c6097e..2ce0137f6f6988ea0621c201ab696f825e27ed42. - PASS: exact-head CI run
33179780715: Desktop Core, all four Desktop Smoke E2E shards, Desktop E2E Integration, macOS build, release candidate, aggregate Desktop, and DCO. - INCOMPLETE: separate local full-suite attempts exceeded reviewer harness limits under concurrent load; exact-head package CI is green. This is a reviewer/tooling confidence gap, not additional author action.
- NOT RUN: native Tauri/AX observation. Browser-level focus/caret/keyboard assertions exist, but no native semantic receipt was obtained; reviewer/tooling owns this optional confidence gap.
Verification owner: author for the causal profile-refresh/persistence regression; reviewer reruns affected Desktop gates and that journey.
Residual risk: native WebKit/AX behavior remains unwitnessed locally, but it is not the reason for this verdict; the source-level persistence defect above is.
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Verdict: REQUEST CHANGES
Reviewed exact head 2ce0137f6f6988ea0621c201ab696f825e27ed42 against base b49675894b39f87215e4dfc8c1ad4a3c28c6097e.
[P2] Preserve the identity of the automatic prefix when an agent's display name changes
MessageComposer overwrites the draft-key entry in implicitAgentMentionPrefixByDraftRef on every render from the current lockedAgents display names (desktop/src/features/messages/ui/MessageComposer.tsx:473-480). Those names are recomputed from live profile/directory data (desktop/src/features/messages/ui/useAgentAddressLockPicker.ts:103-125). Restoration likewise searches only for the current display name and prepends it when absent (useAgentAddressLockPicker.ts:372-424), while draft cleanup strips only the latest recomputed string (useDraftPersistSnapshot.ts:129-135,201-217).
A normal async identity refresh therefore breaks the promised draft invariant:
- Automatic addressing inserts
@OldNameand the user types draft text. - The same pubkey resolves to
NewNameafter profile/directory refresh. - Restoration sees no
@NewName, prepends it, and the render overwrites the recorded prefix with@NewName. - Persistence strips
@NewNamebut leaves the synthetic@OldNamein the saved authored draft. - A later restore can display both recipient-looking prefixes even though only one pubkey is addressed.
The same mutable-prefix model also cannot distinguish a reintroduced synthetic prefix from an authored identical leading mention after automatic addressing is toggled off and on. The stable-name duplicate E2E at desktop/tests/e2e/persistent-agent-audience.spec.ts:1103-1138 does not cover either lifecycle transition.
Author action: record the exact synthetic insertion provenance at the editor transaction/insertion boundary rather than deriving and overwriting it from the current lock list. Add regressions for (a) one locked pubkey changing from one non-null display name to another while authored text exists and (b) off/on re-enable followed by an authored identical leading mention. Across persistence/remount, assert no duplicate or leaked synthetic prefix, preservation of authored text, and the same exact recipient pubkey.
Exact-head Desktop Core, smoke E2E, integration E2E, build, release-candidate, and DCO checks are green. This is a source-level persistence/lifecycle defect not exercised by those gates.
|
🤖 Fixed the profile-refresh draft edge case at
Validation: full desktop unit suite 5,763/5,763, @jedwards27 Please re-review the new head when ready. |
|
@buzz-security-review cf8bcef |
|
🤖 Added Carl’s additional off/on lifecycle regression at |
|
@buzz-security-review a530f29 |
jedwards27
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Reviewed exact head a530f29dbbef34bd774430aa14b7eef29c07439f against base b49675894b39f87215e4dfc8c1ad4a3c28c6097e.
The profile-rename blocker from the previous head is source-addressed: automatic-prefix provenance is now captured at insertion, and restoration checks existing mention identity by pubkey. Two deterministic draft-integrity failures remain in the same provenance lifecycle.
[P2] Clear automatic-prefix provenance when its synthetic mention is removed
onImplicitPrefixInserted stores one prefix per draft (desktop/src/features/messages/ui/MessageComposer.tsx:467-473), but removing the automatic lock deletes the visible prefix without clearing or updating that stored provenance (desktop/src/features/messages/ui/useAgentAddressLockPicker.ts:173-197). Draft persistence later strips whatever still matches the stale record (desktop/src/features/messages/ui/useDraftPersistSnapshot.ts:129-135,201-217).
A real mock-bridge Playwright journey reproduced this at the reviewed head:
- Enable automatic
@Morgarita. - Remove the lock, leaving the composer empty.
- Manually author
@Morgarita manual after removalwhile the agent remains unpinned. - Navigate away to persist the draft.
Expected stored content: @Morgarita manual after removal. Actual: manual after removal. The authored mention is silently deleted.
Author action: clear/update the draft's synthetic-prefix provenance whenever that exact synthetic prefix is removed, including lock removal and typed deletion/synchronization paths, without clearing provenance for a currently restored automatic prefix. Add a remove → identical manually authored mention → persist/remount regression.
[P2] Accumulate provenance for multiple automatic agents instead of replacing it
Each insertion reports only its newly inserted text (desktop/src/features/messages/ui/useAgentAddressLockPicker.ts:247-255,430-439), and MessageComposer replaces the draft's stored prefix with that value (MessageComposer.tsx:467-473). Persistence strips exactly that latest value (useDraftPersistSnapshot.ts:129-135; desktop/src/features/messages/lib/stripImplicitAgentMentions.ts:6-14).
A strengthened multi-agent browser journey failed 3/3 at this head: after automatically adding Morgarita, then Vogue, and authoring draft text, the editor correctly showed both automatic mentions, but navigation persisted @Morgarita draft text instead of draft text. Adding Vogue overwrote the @Morgarita provenance, turning synthetic UI into durable authored content.
Author action: track the complete current synthetic leading prefix—or equivalent identity/range provenance—across add/remove/re-add/rename. Add a multi-agent persistence/remount regression proving that stored content excludes every synthetic mention while visible restoration and exact recipient pubkeys preserve both agents.
Exact-head evidence
- PASS: focused address-lock/prefix tests, 20/20.
- PASS: fresh E2E build and typecheck.
- PASS: six focused browser journeys covering disabled/archive restore, keyboard toggle, single-agent draft hygiene, authored duplicate, off/on duplicate, and multi-word separator/caret, 6/6.
- PASS: exact-head CI, including Desktop Core, all smoke and integration shards, aggregate Desktop, macOS build, release candidate, DCO, and authorization.
- PASS: remote PR head matched local clean
HEADafter testing. - NOT RUN: native Tauri/WebKit accessibility/focus observation. Browser keyboard/caret/accessibility assertions passed; native semantics remain a reviewer-owned confidence gap, not author action.
Verification owner: author supplies the causal regressions and fix; reviewer reruns both failing journeys, affected full Desktop package gates, and exact-head CI.
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
REQUEST CHANGES at exact head a530f29dbbef34bd774430aa14b7eef29c07439f (base b49675894b39f87215e4dfc8c1ad4a3c28c6097e).
Blocking findings
P2 — Preserve all generated automatic mentions when persisting a draft
Each automatic-agent insertion calls onImplicitPrefixInserted with only the newly inserted fragment (desktop/src/features/messages/ui/useAgentAddressLockPicker.ts:245-255,430-439). MessageComposer replaces the per-draft provenance entry with that fragment (desktop/src/features/messages/ui/MessageComposer.tsx:467-473), and draft persistence strips exactly that one string (desktop/src/features/messages/ui/useDraftPersistSnapshot.ts:129-135,201-217).
With automatic agent A followed by B, the composer becomes @B @A draft, but persistence records only @B and saves synthetic @A draft as authored content. Returning to the composer can then restore both automatic agents around the leaked prefix, producing a duplicate/stale visible mention and potentially stale mention-reference identity.
Required change: maintain the complete current generated leading prefix, or equivalent identity/range provenance, across add/remove/re-add/rename. Add a multi-agent persistence/remount regression proving stored draft content excludes every synthetic mention while the restored composer and exact recipient pubkeys retain both agents.
P2 — Clear or update provenance when an automatic mention is removed
onImplicitPrefixInserted stores provenance per draft (MessageComposer.tsx:467-473), but removeAddressedAgent removes the visible automatic prefix without clearing or updating that entry (useAgentAddressLockPicker.ts:173-197). If the user then manually authors the same leading mention and leaves the composer, persistence still sees the stale generated prefix and removes the user’s text.
Reproduction: enable automatic @A; remove A so the composer is empty; manually type @A authored; navigate away. The saved draft becomes authored, silently deleting the authored mention.
Required change: update synthetic-prefix provenance whenever the corresponding generated text is removed, including lock removal and typed deletion/synchronization paths. Add remove → identical manually authored mention → persist/remount coverage.
Coverage
The previous profile-rename blocker is addressed: provenance is captured at insertion and restoration recognizes stable pubkey identity. The team also traced root/thread isolation, exact-recipient delivery, picker/cursor behavior, disabled/archived presentation, membership refresh, and send-time revalidation; no additional blocker was verified. Exact-head required CI is green. Review was read-only GitHub source/diff inspection; no PR code was executed.
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Keep successfully cached managed and relay agent directories usable while background refreshes run, including the matching members-sidebar classification. This intentionally changes autocomplete from the fail-closed-on-refetch policy introduced by #5681: autocomplete is only a hint, while send-time revalidation still fetches authoritative evidence and fails closed before any send. Add coverage for refetch stability and the existing relay-agent invite-as-bot send flow. Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
REQUEST CHANGES at exact head 593f8d17587d4fd85b3c4384c0ba222e779312ca (base 2f66ee24a0bf5efa958378069a6b155e99a1d068).
P2 — Clear generated-prefix ownership when the user deletes an automatic mention
The editor synchronization path notices when a previously visible locked agent mention disappears and excludes that pubkey, but it does not clear the generated fragment owned by that pubkey (desktop/src/features/messages/ui/useAgentAddressLockPicker.ts:149-175). The separate toolbar-removal path does call onImplicitPrefixRemoved when it removes the prefix (useAgentAddressLockPicker.ts:178-213), so these two equivalent removal transitions leave different provenance state.
That stale fragment remains in useImplicitAgentMentionProvenance (desktop/src/features/messages/ui/useImplicitAgentMentionProvenance.ts:13-20,41-50) and draft cleanup later treats any matching leading text as generated (desktop/src/features/messages/ui/useDraftPersistSnapshot.ts:129-135,201-217). A user's authored text can therefore be silently changed:
- Automatic addressing inserts
@Morgarita. - Select all and press Backspace; the mention and lock disappear.
- Manually type
@Morgarita manual after typed deletion. - Navigate away so the draft persists.
The stale record still owns @Morgarita , so the stored draft becomes manual after typed deletion. The existing typed-deletion journey re-adds and sends immediately (desktop/tests/e2e/persistent-agent-audience.spec.ts:1024-1061), while the persistence regression uses toolbar removal (:1141-1172); neither crosses typed deletion into persistence. The hook tests likewise assert audience exclusion without observing provenance removal (desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs:521-618).
Required change: clear the generated fragment by stable pubkey when text synchronization observes that automatic mention removed, while retaining fragments still present. Add a typed-delete → identical manual reauthor → persist/remount regression asserting the authored mention survives, the automatic lock stays absent, and recipients remain accurate.
The latest head fixes the earlier multi-agent accumulation and toolbar-removal defects. Stable-pubkey rename handling, off/on restoration, and current mention/caret behavior showed no additional blocker in read-only exact-head review. Exact-head Desktop Core, all smoke and integration shards, aggregate Desktop, macOS build, release candidate, and DCO are green, but they do not exercise this lifecycle.
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
|
🤖 Fixed the typed-deletion provenance gap at d312ca3. When editor synchronization sees an automatic mention disappear, it now clears that pubkey’s generated fragment before excluding the automatic lock—the same ownership cleanup used by the toolbar remove path.\n\nAdded the reproduced journey: automatic mention → Select All/Backspace → manually type the identical mention → navigate away. It now persists the full authored mention and leaves the automatic lock absent.\n\nValidation: typecheck, fresh E2E build, and all six focused draft-lifecycle journeys passed.\n\n@wesbillman @jedwards27 Please re-review the current head when ready. |
|
@buzz-security-review d312ca3 |
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — APPROVE at exact head d312ca3fb2fe13b277efed091a8a703ffa3170d4 against base 2f66ee24a0bf5efa958378069a6b155e99a1d068.
The prior typed-deletion blocker is resolved. When editor synchronization observes a formerly tracked automatic mention disappear, the implementation now clears generated-prefix provenance for that stable pubkey before recording the exclusion (desktop/src/features/messages/ui/useAgentAddressLockPicker.ts:149-177). A later identical manually authored mention is therefore preserved rather than mistaken for generated text. The regression at desktop/tests/e2e/persistent-agent-audience.spec.ts:1141-1180 covers automatic mention → typed deletion → absent lock → identical manual mention → persisted authored text.
Systems and product review found no unresolved author-actionable defect. Reviewed contracts remain coherent across conversation-keyed audience ownership (persistentAgentAudience.ts:47-59), identity-keyed generated-fragment provenance (useImplicitAgentMentionProvenance.ts:11-53), and recipient revalidation/tag intersection before send (useMentionSendFlow.ts:430-461,540-557). Keyboard removal, focus-visible behavior, root/thread isolation, exclusions/re-additions, multi-agent persistence, and authored text preservation were also exercised.
Exact-head evidence:
- Full Desktop units: 5,788/5,788.
pnpm --dir desktop typecheck,check, and production build: PASS.- Fresh E2E build and focused browser journeys for typed deletion, explicit removal, authored duplicates, re-addition, multi-agent persistence, root/thread isolation, caret/chip restoration, persistence/remount, and exact-recipient send: PASS.
- Causal mutation removing the new provenance callback kept the build green but made the targeted regression fail with authored-prefix loss; restoring exact-head code passed with a clean tree.
git diff --check: PASS; remote PR head matched the reviewed SHA.
Confidence gaps:
- Native Tauri/WebKit accessibility observation was not run. Author action: none. Verification owner: reviewer/tooling if native semantic evidence is desired.
- At submission freshness,
Desktop Coreremained in progress; all completed exact-head checks had no failures, and smoke shards 1–4, both Desktop integration shards, macOS build, release candidate, DCO, and authorization were green. Author action: none unless a required gate fails. Verification owner: named CI gate.
Superseded by the typed-deletion provenance fix at d312ca3, reviewed and approved on the current head.
…-history * origin/main: fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904) perf(mobile): reduce cold startup and channel rendering delays (#6996) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
**Category:** improvement **User Impact:** Automatic mentions are easier to turn off and now behave consistently across conversations, settings, drafts, and repeated agent mentions. **Problem:** People found the new automatic mention behavior hard to control: turning it off in Settings did not reliably affect the composer, removing a mention could require also disabling the feature, and root/thread composers could inherit or restore surprising state. Other reported rough edges included only one of several mentioned agents becoming automatic, synthetic mentions leaking into drafts, restored mentions corrupting adjacent text, controls remaining visible in archived channels, and unclear picker feedback. See the [original feedback thread](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=c949ec399274fbb0d6633da3f95712e843a67dcada214f63d72f6975b406604b). **Solution:** Polish the existing feature around the problems people encountered, keeping automatic mentions controllable and scoped to the active conversation. | Reported issue | UX fix | | --- | --- | | Turning automatic mentions off in Settings did not reliably update the composer. | The global setting and composer control stay synchronized, and disabling the feature does not clear typed text. | | Removing an automatic mention could require both deleting the mention and turning off the feature. | Removing or unchecking an agent excludes that agent for the current conversation, while explicitly re-adding the agent can restore automatic mention behavior. | | Root and thread composers could share or restore surprising selections. | Each root or thread composer keeps its own automatic audience and restores it when the user returns. A request to enable automatic mentions only in agent threads was considered; this PR keeps them available at the channel root but prevents state from leaking between the two. | | Mentioning multiple agents could leave only one saved as automatic. | Multi-agent selections remain represented in the automatic audience and restored mention chips. | | Automatic mention prefixes could be saved as if the user typed them. | Synthetic prefixes stay out of persisted drafts while authored text is preserved. | | Restored mentions could lose their separator and corrupt continued typing. | Restored multi-word mentions retain their trailing space and place the caret after it. | | Archived channels showed automatic-mention state beside a disabled composer. | Disabled composers hide automatic-mention controls while preserving the draft and restoring state when re-enabled. | | Confirmation and picker behavior made the feature feel difficult to inspect or adjust. | Confirmations dismiss with removed agents, remain open while hovered, and expose the setting before it changes; pin icons, contrast, scope copy, animation, and keyboard toggling are also clarified. | | Agent suggestions and membership state could shift during directory refreshes. | Suggestions and membership labels stay stable during refreshes, while send-time authorization still revalidates access. | ## Changes <details> <summary>File changes</summary> **desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs** Adds coverage for the channel-roster eligibility rules used by agent mention autocomplete. **desktop/src/features/agents/lib/agentAutocompleteEligibility.ts** Aligns agent autocomplete eligibility with channel membership so available agents and their labels stay trustworthy. **desktop/src/features/channels/ui/MembersSidebar.tsx** Uses the shared member-pubkey logic when presenting and acting on channel members. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs** Covers preference changes that must remain stable while composer controls are toggled. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts** Keeps the automatic-mention preference as durable user intent rather than transient composer state. **desktop/src/features/messages/lib/mentionMemberPubkeys.ts** Centralizes which member identities count as mentionable in the current channel. **desktop/src/features/messages/lib/persistentAgentAudience.test.mjs** Expands lifecycle coverage for persistent agent audiences, explicit exclusions, and restored mentions. **desktop/src/features/messages/lib/persistentAgentAudience.ts** Models automatic, explicit, and excluded agent audiences separately so user choices survive updates without leaking across composers. **desktop/src/features/messages/lib/stripImplicitAgentMentions.test.mjs** Verifies implicit automatic mentions are removed without damaging surrounding separators or authored content. **desktop/src/features/messages/lib/stripImplicitAgentMentions.ts** Strips presentation-only automatic mentions before draft persistence while preserving whitespace and authored text. **desktop/src/features/messages/lib/useMentions.ts** Routes mention insertion and removal through the composer-local audience lifecycle. **desktop/src/features/messages/lib/useRichTextEditor.ts** Preserves mention-chip structure and caret placement when automatic mentions are restored. **desktop/src/features/messages/ui/ComposerAddressControls.test.mjs** Updates control-state expectations for disabled automatic mentions and restored pin affordances. **desktop/src/features/messages/ui/ComposerAddressControls.tsx** Makes automatic-mention state, disabled presentation, and pin controls visually explicit. **desktop/src/features/messages/ui/MentionAutocomplete.test.mjs** Adds coverage for roster labels, pin state, and picker behavior after mention selection. **desktop/src/features/messages/ui/MentionAutocomplete.tsx** Keeps the shortcut picker open for repeated selection and restores visible automatic-mention pin indicators. **desktop/src/features/messages/ui/MessageComposer.tsx** Scopes automatic mention state to each root or thread composer and coordinates restoration, draft persistence, and sending. **desktop/src/features/messages/ui/MessageComposerToolbar.tsx** Passes the effective automatic-mention state into the toolbar presentation. **desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs** Updates keyboard interaction coverage for toggling agents in place. **desktop/src/features/messages/ui/useAddressedAgentMentionRestore.ts** Restores automatic mention chips after lifecycle changes without moving or duplicating authored content. **desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs** Substantially expands coverage for toggles, exclusions, synchronization, and picker dismissal rules. **desktop/src/features/messages/ui/useAgentAddressLockPicker.ts** Keeps the picker usable across repeated choices and preserves explicit per-agent intent while settings change. **desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts** Makes the keyboard shortcut toggle the highlighted automatic audience choice without replacing unrelated selections. **desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts** Owns composer-local automatic-mention lifecycle behavior, including restoration, exclusions, deletion, and disabled-state handling. **desktop/src/features/messages/ui/useComposerMentionPicker.test.mjs** Adds focused picker lifecycle coverage for selection, hover, and dismissal behavior. **desktop/src/features/messages/ui/useComposerMentionPicker.ts** Prevents premature picker dismissal while the user is interacting with its controls. **desktop/src/features/messages/ui/useDraftPersistSnapshot.ts** Persists only user-authored draft content rather than implicit automatic mention decorations. **desktop/src/shared/lib/keyboard-shortcuts.ts** Updates the automatic-mention shortcut description to match its toggle behavior. **desktop/src/testing/e2eBridge.ts** Extends the desktop test bridge with the state needed to exercise roster and automatic-mention transitions. **desktop/tests/e2e/mentions.spec.ts** Covers roster-based labels, managed-agent invitation, revocation, and recovery behavior in the complete mention flow. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Adds end-to-end coverage for root/thread isolation, preference synchronization, manual exclusions, draft hygiene, restored chips, separators, hover behavior, and disabled presentation. </details> ## Reproduction Steps 1. Open a channel with at least two available agents and enable automatic mentions from the composer mention control. 2. Select multiple agents, remove or uncheck one, and confirm subsequent composer updates keep that agent excluded while the others remain automatic. 3. Open a thread, choose a different automatic audience there, and switch between the thread and root composer; confirm each composer retains only its own choices. 4. Disable automatic mentions and confirm the draft text remains unchanged while automatic chips and controls show the disabled state; re-enable the setting and confirm eligible automatic chips return. 5. Delete an automatic mention chip, then explicitly add the agent again; confirm it immediately returns as an automatic mention without disturbing spaces or the caret, including for a multi-word name. 6. Reload with a saved draft and confirm implicit automatic mentions were not persisted as authored draft text. 7. Use the automatic-mention keyboard shortcut and picker repeatedly; confirm the picker remains open for additional choices and the highlighted agent toggles in place. ## Validation Validated at `34d208b47d64a9816f88e10a46bcfd479e917d75` after rebasing onto `origin/main` (`69096c9a8`): - Desktop unit tests: 5,731 passed, 0 failed. - Desktop TypeScript typecheck: passed. - Desktop E2E build: passed; emitted only existing chunk and dynamic-import warnings. - `pnpm check`: exited successfully; 4 warnings and 5 informational findings are in unrelated files introduced by current main. ## Screenshots/Demos The behavioral changes are covered by the focused desktop E2E scenarios above. Screenshots can be attached from the screenshot-producing automatic-mention E2E after the PR is created. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com>
…c-agent-commit-identity * origin/main: fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904) perf(mobile): reduce cold startup and channel rendering delays (#6996) feat(mobile): push notifications MVP (#6269) refactor(db): extract domain stores from database runtime (#6987) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…composer mentions) with execution-nodes
* feat(mobile): push notifications MVP (block#6269) This PR implements MVP, iOS-only, [NIP-PL](https://github.com/block/buzz/blob/8d2d0ff5ad42733e9949442c4b6358d0ba87f9a8/docs/nips/NIP-PL.md)-compliant push notifications. A relay with `BUZZ_PUSH_ENABLED` will send a push notification for any message that appears in the in-app Notifications tab. ## Enrollment flow The first time the client first connects to a relay with `BUZZ_PUSH_ENABLED`: ```mermaid sequenceDiagram autonumber participant App as Buzz iOS app participant iOS participant Relay as Buzz relay participant Attest as Apple App Attest participant Gateway as Push gateway App->>Relay: Fetch NIP-11 push capability Relay-->>App: Push profile, current relay public key, and limits par App->>iOS: Request notification permission iOS-->>App: Permission result and App->>iOS: Register for remote notifications iOS-->>App: Device token end App->>Gateway: Request installation challenge Gateway-->>App: Single-use challenge App->>Attest: Attest installation transcript Attest-->>App: Attestation proof App->>Gateway: Enroll device token and proof Gateway-->>App: Installation handle App->>Gateway: Request delegation challenge Gateway-->>App: Single-use challenge App->>Attest: Assert relay-key delegation Attest-->>App: Assertion App->>Gateway: Create delegation Gateway-->>App: Opaque endpoint grant App->>Relay: Publish encrypted push lease and filters Relay-->>App: Lease acknowledged ``` ## Push-time flow When a notification-eligible event is received by the relay: ```mermaid %%{init: { "sequence": { "actorMargin": 20, "width": 110, "messageMargin": 18, "diagramMarginX": 8, "wrap": true } }}%% sequenceDiagram autonumber participant Relay as Buzz relay participant Gateway as Push gateway participant APNs as Apple Push<br/>Notification service participant iOS participant NSE as Notification service<br/>extension Relay->>Gateway: POST /v1/deliveries/apns<br/>opaque endpoint grant, request ID, expiry, NIP-98 authorization Gateway->>APNs: POST /3/device/{device-token}<br/>topic, request ID, expiry, constant mutable-content payload APNs-->>Gateway: 200 OK: request accepted Gateway-->>Relay: 200 OK: accepted status APNs-->>iOS: Notification: constant reconnect alert<br/>mutable-content = 1 iOS->>NSE: Invoke extension<br/>original notification content NSE->>Relay: POST /query: subscription filters, limit 10<br/>NIP-98 authorization Relay-->>NSE: 200 OK: signed Nostr events<br/>kinds 9, 40002, 45001, or 45003 NSE->>iOS: Complete notification: title, body, subtitle<br/>thread ID, exact-message target ``` relay → push gateway → APNs -> NSE -> Notification Center ## Known limitations The APNs wake payload is intentionally constant and opaque: it contains no originating community or message identifier, in keeping with the implemented NIP-PL privacy design. The Notification Service Extension must therefore reconnect to the relay and resolve eligible messages after each wake. Around overlapping wakes, timing boundaries, or resolution windows, notification presentation may occasionally omit an expected message or display a message more than once. This best-effort behavior is deliberately accepted for the current implementation and will be measured during the internal rollout to determine whether the user experience is acceptable before any broader deployment; the implementation does not claim exactly-once presentation. ## Validation Live end-to-end hardware validation used an internal remotely hosted development relay and push gateway, the APNs sandbox, and a physical iPhone 12 mini: - A second real Buzz client published a uniquely marked message through the hosted relay. - The relay matched the message and sent the constant opaque wake through the hosted gateway. The gateway made an actual APNs request; no `simctl push` or simulated notification was used. - The iPhone received the notification on its lock screen. The Notification Service Extension reconnected to the relay, fetched the event, verified its ID and signature, and replaced the placeholder content with the real notification title and body. - After the app populated its shared presentation cache, a final marked notification visibly showed the sender display name, sender avatar, and hashtag-prefixed channel name. - Tapping a lock-screen notification opened Buzz and exercised the notification-response path and navigated to the corresponding message. Final validation with a dogfood-signed artifact and production App Attest/APNs configuration remains a release step. ## Independent pre-reviews - **First pass:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=74ab9a93bb227f3e762568f1cf9fee66d7495b0edc3918735ff787238b9cc585) found missing transient retries, executor-key rotation suppression, duplicate installation renewal, and an unauthenticated challenge write amplifier. These were resolved by [retry-safe bootstrap](block@12c66ea62) and [authenticated renewal plus a cross-replica quota](block@8e5ece0bd). [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=d26d53daa4684669e2ed354638241f13f36c3a97027fe8b4dd738aff09038962) found delegation generation burning and an edited applied migration, resolved by [exact-generation revocation](block@c26d2159d) and a [forward-only migration](block@956c1d099). [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=d43139138a0b15f806cbdbeeedd8f69d992cadf2e805876db6fdde6a34c7eda1) found no blockers. - **Exact-head re-review:** [Carl](buzz://message?channel=18882f4c-289f-41db-942f-81f6f8066da1&id=a897721673459301b0cf26e8b85a1478d7ebbb56a4621f93d774c98d395b8f68), [sol-max](buzz://message?channel=ad83385f-8e9e-4461-9a35-c1bf2e208532&id=fb2159f709ec68f74f7b21459acd76da0e8a7c5c0f3d469f99826b0cc2380849), and [k3-max](buzz://message?channel=5e46055d-a766-4065-ae25-05d1e4aaa6b2&id=2ce2842910435f562e9d9cc718595848f281b122c94605e523a4b964254b8bfb) independently returned **NO BLOCKERS** at `7eb3a650b`; k3-max also revalidated every remediation and the endpoint-specific App Attest enrollment bound. --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com> * perf(mobile): reduce cold startup and channel rendering delays (block#6996) Pinky is opening this PR on Wes’s behalf. ## Summary Reduce two separately measured mobile delays without changing the relay API or removing rich message rendering: - Publish the finite channel-list snapshot without waiting for live subscription setup. - Batch active channel-list subscriptions into sorted, deterministic chunks of at most 128 explicit channel IDs, retaining unchanged chunks. - Install replacement chunks before retiring old coverage. Retain old chunks across thrown replacement failures; filter callbacks to the current relay/identity and still-desired channels; clean up retired/in-flight work across disconnect and disposal. - Scope the custom-emoji Markdown matcher to known shortcodes actually referenced in the rendered content, rather than embedding the whole community palette in every message’s regex. Preserve unknown literals, shared-colon token boundaries, event-tag URL priority, content edits, and code literals. - Honor explicit zero retry hints without inventing a ten-second session-wide gate, while preserving the ordinary live-subscription retry backoff and any already-active gate. ## Matched performance results Medians of three before and three after process-cold launches, alternated on the same authenticated iPhone 17 Pro / iOS 26.5 simulator. Before is mobile source at `e76c81968b65b0755b83efdd59dc3375c59ddf40`; after is this production patch before two documentation-only comment fixes. First channel-list frame: 11.617s → 3.179s · 73% lower latency Live setup duration: 8.475s → 0.185s · 98% lower latency Channel-open first message-list frame: 2.754s → 1.230s · 55% lower latency Message data ready → first frame: 1.977s → 0.286s · 86% lower latency Channel-open reveal complete: 2.845s → 1.394s · 51% lower latency Channel-open data readiness: 0.770s → 0.944s · 23% higher latency The gain is client-side orchestration/rendering, not a claim that the relay became faster. First channel-list frame ranges were 10.835–11.788s before and 2.872–3.395s after; channel-open first-frame ranges were 1.560–2.906s before and 1.149–1.317s after. ### Measurement boundaries - Debug simulator builds, CPU sampling disabled, bounded timestamp probes enabled identically. These are not release/physical-device measurements. - Startup clock starts at Dart `main`; build/install/native pre-main time is excluded. Auth/preferences and OS/disk caches are retained between new processes. - Same account scale: 113 active channels. Latest-message events varied slightly with live activity (1543–1546). - Channel-open uses the same initial 50-row history window, 97 query events, and 67 provider events. The 2306-entry emoji palette is explicitly loaded before navigation on both sides; palette preparation is excluded from the channel-open clock and happens after the startup frame measurement. - Both diagnostic builds temporarily disabled unused avatar segmentation to work around the existing Google ML Kit arm64-simulator slice limitation. The workaround, dependency/native changes, auto-navigation, and all probes are excluded from this PR. ## Validation - Full mobile package suite: `flutter test` — 1890 passed. - `just mobile-check` — 506 files unchanged; analyzer clean. - `just file-size-check` — policy tests and all client ratchets passed. - `git diff --check` — passed. - New lifecycle regressions cover front-sorting insertion across a chunk boundary while replacement readiness is paused, failure retention/departed-channel filtering, retired generation + disconnect cleanup, disposal, chunk limits, unchanged-set reuse, and scope switches. - Emoji unit/widget coverage includes a 2500-unused-emoji palette, unknown tokens, case matching at the component level, shared-colon boundaries, rich text, event URL priority, and content edits. - Fresh-frame source review traced the subscription queue/fences, callback scopes, duplicate-event paths, matcher/wiring, and retry scheduling. - At committed/pushed head `13a83b628c8411c5885e6f76a250ba87accf6067`, all normal pre-push hooks passed: `mobile-checks` (formatter, analyzer, and the full 1890-test mobile suite), `file-size-check`, `branch-skew`, and `push-head-scope`. The commit hook formatted 506 files with no changes. Runtime measurements preceded only the two documentation-comment fixes; no runtime source changed afterward. ## Limits / follow-ups - `RelaySession.subscribe` still settles under its existing EOSE/fallback/retryable-CLOSED contract. “Setup completed” is not an unconditional EOSE or live-delivery guarantee. This PR does not add status-aware replacement ownership. - The channel-message provider still awaits subscribe before fetching history; that separate serialization is not removed here. - Oversized Huddle queries and the separate history batching path above 128 active channels remain follow-ups, as do pre-existing read-state initialization/size warnings. - Palette-only widget refresh and upstream Markdown uppercase-dispatch behavior are not changed. - A clean source build still has the existing Google ML Kit arm64-simulator issue; the profiling workaround is not a proposed product fix. Originating Buzz conversation: buzz://message?channel=793b0522-7995-4375-b1a6-fd94a96fa21d&id=6ba88afdec78ab2cfb6728afcd4a6d10f29e6aa33ff0f62f45d6750381e4d789 --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz> * fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (block#6904) ## Why PR block#6330 split agent harness/runtime detection into a cheap (cache-only) path and a forced (spawning) path. Two regressions followed, both surfacing as every harness showing "(not installed)" / "CLI missing" across the agent create/edit picker, Agents > Agent defaults, and Settings > Agents — blocking agent create/edit until the user clicked Install in Settings > Agents. ## Root cause One underlying bug, two victims: - **Boot false-negative.** The resolve cache is in-memory, so it starts cold on every launch. `resolve_command_cached` (the cheap path) consulted only the Buzz-managed shim dirs plus that cold cache, and `buzz_managed_command_path`'s allowlist structurally excludes `buzz-agent`. The bundled sidecar could therefore never resolve on the cheap path until a forced pass warmed the cache, so cheap-path surfaces rendered all-missing at boot. App setup never warms the cache. - **"Check again" hang.** `run_in_login_shell` used an untimeouted `Command::output()`; a wedged login shell froze the whole forced pipeline, leaving "Check again" spinning forever. ## What - `resolve_command_cached` now also calls `resolve_workspace_command`, resolving the bundled sidecar via a filesystem stat (no spawn) — the same class of work the managed-shim check already performs. `buzz-agent` can no longer report missing, even inside the boot warm window. - New `discovery/bounded_command.rs` runs any discovery child under a hard wall-clock deadline, polling with `try_wait` rather than blocking on `wait()`. Stdout and stderr are piped to two drain threads whose buffers share an aggregate `CAPTURE_LIMIT`; a breach fails closed (kill the tree, return `None`), so a noisy or hostile probe can force neither unbounded memory nor disk fill. Tree teardown runs on every exit path — timeout, error, cap breach, *and* success — because a login-shell rc file or auth CLI can legitimately background a descendant that would otherwise outlive discovery. Ownership is deliberately asymmetric: - **Unix:** the child leads its own process group (`process_group(0)`); teardown is `SIGTERM` → bounded grace → `SIGKILL` on the group. A descendant that leaves the group (`setsid`/`setpgid`) while holding a pipe is not owned and may survive one probe, but can never hang or unbound the helper: the Unix drains read nonblocking and end on `WouldBlock` once teardown sets the stop flag, so the join returns promptly without waiting on an escaped writer's EOF. - **Windows:** the child is spawned `CREATE_SUSPENDED`, assigned to a kill-on-close Job Object while frozen, then resumed. The job owns the root before any descendant can exist and is created without breakaway, so no writer can escape — a hard whole-tree guarantee, and closing the job reaps the tree even after the root has exited. Any failure to create, assign, or resume is fail-closed: the child is terminated and reaped and the spawn returns `None` (discovery treats it as command-not-found) rather than running unowned. - Each login-shell candidate is bounded by a 10s timeout via that helper, falling through to the next candidate on timeout instead of aborting the resolve. The login-shell path cache is generation-aware: a probe that loses to a concurrent refresh or lands mid-refresh returns the authoritative cached value (or re-probes under the new generation) rather than its own rejected local result, so a losing thread can never settle the UI with a PATH-missing catalog while the cache holds a fresh success. - Warm the ACP runtime catalog once at `AppShell` mount and gate the cheap-path surfaces on that pass. A module-level boot-warm state (`idle` → `pending` → `settled`/`failed`, deduped per launch) lets `useAcpRuntimesQuery` present a cold catalog as *loading* while the first forced pass runs and as a *retryable error* (carrying the probe's real reason) if it fails, instead of blessing "every harness not installed" as authoritative. A non-empty catalog always wins, so a revalidation or later failure never blanks a good list; the gate only overlays once the warm has started, so onboarding (which renders before the warm) is unaffected. Deduping per launch also fixes the previous per-remount re-fire. ## Verification Unix teardown and the drain contract are runtime-proven by `#[ignore]`-free tests that record a backgrounded descendant's real PID and assert the helper returns promptly on both the success and timeout paths without blocking on that writer. The generation-aware login-shell cache is covered by deterministic tests through a `cfg(test)` injectable probe seam that assert the function's return value under both concurrent-refresh interleavings — the losing caller returns the peer's committed success, and a mid-probe refresh forces a re-probe to the fresh value. The Windows ownership contract has no CI lane, so `bounded_command.rs` carries two `#[ignore]`-gated tests (spawn/assign race, looped; and the timeout path) for a sanctioned run on a Windows host. The boot-warm gate is covered by unit tests for the pure overlay and the `startBootWarm` failure → retry → settle lifecycle. Origin: [Buzz thread](buzz://message?channel=5ef5d5bb-643f-4b87-bbf4-e8b64585ffeb&id=a4b1c4485de4d35cff0f914d4f4211c796f44f670e76de2ef9431f7e882c906e) Fixes block#6872 Related block#6662 --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> * fix(composer): polish automatic mentions (block#6956) **Category:** improvement **User Impact:** Automatic mentions are easier to turn off and now behave consistently across conversations, settings, drafts, and repeated agent mentions. **Problem:** People found the new automatic mention behavior hard to control: turning it off in Settings did not reliably affect the composer, removing a mention could require also disabling the feature, and root/thread composers could inherit or restore surprising state. Other reported rough edges included only one of several mentioned agents becoming automatic, synthetic mentions leaking into drafts, restored mentions corrupting adjacent text, controls remaining visible in archived channels, and unclear picker feedback. See the [original feedback thread](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=c949ec399274fbb0d6633da3f95712e843a67dcada214f63d72f6975b406604b). **Solution:** Polish the existing feature around the problems people encountered, keeping automatic mentions controllable and scoped to the active conversation. | Reported issue | UX fix | | --- | --- | | Turning automatic mentions off in Settings did not reliably update the composer. | The global setting and composer control stay synchronized, and disabling the feature does not clear typed text. | | Removing an automatic mention could require both deleting the mention and turning off the feature. | Removing or unchecking an agent excludes that agent for the current conversation, while explicitly re-adding the agent can restore automatic mention behavior. | | Root and thread composers could share or restore surprising selections. | Each root or thread composer keeps its own automatic audience and restores it when the user returns. A request to enable automatic mentions only in agent threads was considered; this PR keeps them available at the channel root but prevents state from leaking between the two. | | Mentioning multiple agents could leave only one saved as automatic. | Multi-agent selections remain represented in the automatic audience and restored mention chips. | | Automatic mention prefixes could be saved as if the user typed them. | Synthetic prefixes stay out of persisted drafts while authored text is preserved. | | Restored mentions could lose their separator and corrupt continued typing. | Restored multi-word mentions retain their trailing space and place the caret after it. | | Archived channels showed automatic-mention state beside a disabled composer. | Disabled composers hide automatic-mention controls while preserving the draft and restoring state when re-enabled. | | Confirmation and picker behavior made the feature feel difficult to inspect or adjust. | Confirmations dismiss with removed agents, remain open while hovered, and expose the setting before it changes; pin icons, contrast, scope copy, animation, and keyboard toggling are also clarified. | | Agent suggestions and membership state could shift during directory refreshes. | Suggestions and membership labels stay stable during refreshes, while send-time authorization still revalidates access. | ## Changes <details> <summary>File changes</summary> **desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs** Adds coverage for the channel-roster eligibility rules used by agent mention autocomplete. **desktop/src/features/agents/lib/agentAutocompleteEligibility.ts** Aligns agent autocomplete eligibility with channel membership so available agents and their labels stay trustworthy. **desktop/src/features/channels/ui/MembersSidebar.tsx** Uses the shared member-pubkey logic when presenting and acting on channel members. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs** Covers preference changes that must remain stable while composer controls are toggled. **desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts** Keeps the automatic-mention preference as durable user intent rather than transient composer state. **desktop/src/features/messages/lib/mentionMemberPubkeys.ts** Centralizes which member identities count as mentionable in the current channel. **desktop/src/features/messages/lib/persistentAgentAudience.test.mjs** Expands lifecycle coverage for persistent agent audiences, explicit exclusions, and restored mentions. **desktop/src/features/messages/lib/persistentAgentAudience.ts** Models automatic, explicit, and excluded agent audiences separately so user choices survive updates without leaking across composers. **desktop/src/features/messages/lib/stripImplicitAgentMentions.test.mjs** Verifies implicit automatic mentions are removed without damaging surrounding separators or authored content. **desktop/src/features/messages/lib/stripImplicitAgentMentions.ts** Strips presentation-only automatic mentions before draft persistence while preserving whitespace and authored text. **desktop/src/features/messages/lib/useMentions.ts** Routes mention insertion and removal through the composer-local audience lifecycle. **desktop/src/features/messages/lib/useRichTextEditor.ts** Preserves mention-chip structure and caret placement when automatic mentions are restored. **desktop/src/features/messages/ui/ComposerAddressControls.test.mjs** Updates control-state expectations for disabled automatic mentions and restored pin affordances. **desktop/src/features/messages/ui/ComposerAddressControls.tsx** Makes automatic-mention state, disabled presentation, and pin controls visually explicit. **desktop/src/features/messages/ui/MentionAutocomplete.test.mjs** Adds coverage for roster labels, pin state, and picker behavior after mention selection. **desktop/src/features/messages/ui/MentionAutocomplete.tsx** Keeps the shortcut picker open for repeated selection and restores visible automatic-mention pin indicators. **desktop/src/features/messages/ui/MessageComposer.tsx** Scopes automatic mention state to each root or thread composer and coordinates restoration, draft persistence, and sending. **desktop/src/features/messages/ui/MessageComposerToolbar.tsx** Passes the effective automatic-mention state into the toolbar presentation. **desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs** Updates keyboard interaction coverage for toggling agents in place. **desktop/src/features/messages/ui/useAddressedAgentMentionRestore.ts** Restores automatic mention chips after lifecycle changes without moving or duplicating authored content. **desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs** Substantially expands coverage for toggles, exclusions, synchronization, and picker dismissal rules. **desktop/src/features/messages/ui/useAgentAddressLockPicker.ts** Keeps the picker usable across repeated choices and preserves explicit per-agent intent while settings change. **desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts** Makes the keyboard shortcut toggle the highlighted automatic audience choice without replacing unrelated selections. **desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts** Owns composer-local automatic-mention lifecycle behavior, including restoration, exclusions, deletion, and disabled-state handling. **desktop/src/features/messages/ui/useComposerMentionPicker.test.mjs** Adds focused picker lifecycle coverage for selection, hover, and dismissal behavior. **desktop/src/features/messages/ui/useComposerMentionPicker.ts** Prevents premature picker dismissal while the user is interacting with its controls. **desktop/src/features/messages/ui/useDraftPersistSnapshot.ts** Persists only user-authored draft content rather than implicit automatic mention decorations. **desktop/src/shared/lib/keyboard-shortcuts.ts** Updates the automatic-mention shortcut description to match its toggle behavior. **desktop/src/testing/e2eBridge.ts** Extends the desktop test bridge with the state needed to exercise roster and automatic-mention transitions. **desktop/tests/e2e/mentions.spec.ts** Covers roster-based labels, managed-agent invitation, revocation, and recovery behavior in the complete mention flow. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Adds end-to-end coverage for root/thread isolation, preference synchronization, manual exclusions, draft hygiene, restored chips, separators, hover behavior, and disabled presentation. </details> ## Reproduction Steps 1. Open a channel with at least two available agents and enable automatic mentions from the composer mention control. 2. Select multiple agents, remove or uncheck one, and confirm subsequent composer updates keep that agent excluded while the others remain automatic. 3. Open a thread, choose a different automatic audience there, and switch between the thread and root composer; confirm each composer retains only its own choices. 4. Disable automatic mentions and confirm the draft text remains unchanged while automatic chips and controls show the disabled state; re-enable the setting and confirm eligible automatic chips return. 5. Delete an automatic mention chip, then explicitly add the agent again; confirm it immediately returns as an automatic mention without disturbing spaces or the caret, including for a multi-word name. 6. Reload with a saved draft and confirm implicit automatic mentions were not persisted as authored draft text. 7. Use the automatic-mention keyboard shortcut and picker repeatedly; confirm the picker remains open for additional choices and the highlighted agent toggles in place. ## Validation Validated at `34d208b47d64a9816f88e10a46bcfd479e917d75` after rebasing onto `origin/main` (`69096c9a8`): - Desktop unit tests: 5,731 passed, 0 failed. - Desktop TypeScript typecheck: passed. - Desktop E2E build: passed; emitted only existing chunk and dynamic-import warnings. - `pnpm check`: exited successfully; 4 warnings and 5 informational findings are in unrelated files introduced by current main. ## Screenshots/Demos The behavioral changes are covered by the focused desktop E2E scenarios above. Screenshots can be attached from the screenshot-producing automatic-mention E2E after the PR is created. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com> * fix(desktop): surface channel history load failures (block#7013) ## Summary A failed initial channel-history request no longer appears as an authoritative empty channel. The timeline now shows an announced error with a Retry action, while cached messages remain visible when a later refresh fails; successful empty channels continue to use their normal intro state. ### Related issue None found. ### Testing - Full desktop unit suite (`pnpm test`) - Desktop TypeScript check (`pnpm exec tsc --noEmit`) - Biome checks for changed files - Repository file-size ratchet - Full pre-push desktop checks and tests - Desktop app launched successfully against local Postgres and Redis for manual testing No screenshot is included because the new UI is only shown after a terminal relay-history failure; the regression test pins the error/empty/list precedence directly. --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> * feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (block#7038) ## What Two telemetry additions to make a known silent-death failure mode visible in harness logs. **1. `stop` field on `llm: call completed` INFO** (`crates/buzz-agent/src/llm.rs`) The `ProviderStop` value was already parsed and stored on `LlmResponse` but never emitted in the log line. Without it, "model chose `end_turn`" vs "gateway truncated/refused" is indistinguishable from telemetry alone. **2. WARN on silent-turn signature** (`crates/buzz-agent/src/agent.rs`) Emits a `WARN` when a turn produces no publish, no visible assistant text, and either near-zero or absent output tokens. The WARN logic is extracted into `warn_if_silent_turn` (pure synchronous function) so the seam is testable without the async run loop. Three independent gates before the WARN fires: 1. **`!buzz_reply_call_seen`** — no publish attempt in any round, tracked unconditionally via the existing `is_buzz_reply_call` matcher. Read-only tool calls do NOT suppress the WARN; a turn that ran tools but never published and died at 3 tokens is still a silent death. 2. **`text_is_empty`** — no visible assistant text in the final round. A terse reply like "OK" (≤12 tokens, non-empty) is not a silent death. 3. **Token check** — two distinct WARN messages: - `Some(t) where t <= 12`: near-zero token count, the observed failure signature (2–12 tokens) - `None` usage: provider omitted token counts entirely, separately diagnostic Tests use a scoped `tracing_subscriber` layer (same pattern as the existing stall-warn tests in `llm.rs`) to exercise the WARN seam directly: - Canonical signature (no publish, no text, 4 tokens) → 1 WARN - Non-empty assistant text → 0 WARNs - Publish seen → 0 WARNs - `None` usage (no publish, no text) → 1 WARN ## Why Recurring silent-death incident in a specific agent×channel combination: sessions die with 1 LLM call, 2–12 output tokens, no tool calls, no message, no error — recorded as a "successful" turn. The harness log shows the token count but not the `stop_reason`, leaving the root cause undiagnosable without request-level tracing. The observed shape also includes tool-step-then-3-token-death (one tool call, then silence) — the publish-aware gate catches both shapes. Context thread: buzz://message?channel=91fd9ca1-cf04-4ef7-b18f-aa2aee55692b&id=e3f1693f2e29f26a0c840f8054d592270c1504beacdc1d9c2063d8ab82960a06 ## Scope Logging and telemetry only. No behavior change, no retry-logic change, no stop-reason mapping change. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> * fix(aeon): preserve worker process-group policy Signed-off-by: StephGlansberg <StephGlansberg@users.noreply.github.com> --------- Signed-off-by: Tom Brow <tomb@squareup.com> Signed-off-by: Tom Brow <tomb@block.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Signed-off-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: StephGlansberg <StephGlansberg@users.noreply.github.com> Co-authored-by: Tom Brow <tomb@block.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Jordan Mecom <jm@squareup.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz> Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: thomaspblock <thomasp@squareup.com> Co-authored-by: StephGlansberg <StephGlansberg@users.noreply.github.com>
…on-runtime * origin/main: fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
* origin/main: feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
…age-rw * origin/main: feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) Signed-off-by: Joel Robotham <jrobotham@squareup.com>
* origin/main: (32 commits) fix(acp): wake agents from workflow messages (#6953) feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904) perf(mobile): reduce cold startup and channel rendering delays (#6996) feat(mobile): push notifications MVP (#6269) refactor(db): extract domain stores from database runtime (#6987) 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) ... Signed-off-by: Carl <1f967df5817845a2a5d74c82ac3098dea0bb7342665352af6643c5ac5c878dd3@buzz.block.builderlab.xyz> # Conflicts: # desktop/src/features/channels/ui/ChannelPane.tsx
…n-surface * origin/main: fix(desktop): back split thread headers (#7137) add public descriptions to agent personas (#7126) feat(desktop): add protected-build Bestie experiment (#6902) fix(relay): reject a frame on its own acknowledgement channel (#6961) fix(acp): wake agents from workflow messages (#6953) feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904) perf(mobile): reduce cold startup and channel rendering delays (#6996) feat(mobile): push notifications MVP (#6269) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…enericize * origin/main: fix(desktop): back split thread headers (#7137) add public descriptions to agent personas (#7126) feat(desktop): add protected-build Bestie experiment (#6902) fix(relay): reject a frame on its own acknowledgement channel (#6961) fix(acp): wake agents from workflow messages (#6953) feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…#7061) Mining the last 25 PRs' review threads (45 substantive findings, 11 reviewed PRs, avg **4.8 review rounds** each) shows **53% of findings are repeats** of five clusters: swallowed failures, stale-async-state races, tests that don't bind the production seam, unbounded resources/retry loops, and non-atomic multi-step persistence. PR #6956 alone burned 4 rounds converging on one of these classes. A second, independent mining pass over **71 agent-review rooms (303 findings, Aug 18–29)** confirmed the same clusters and added outcome data — how often authors actually fix each finding class once flagged: test-seam binding and unbounded-resource findings **100%**, swallowed errors **90%**, stale-state races **70%**. It also surfaced two clusters the GitHub-thread pass under-sampled: **assistive-semantics defects** (44 findings, second-largest cluster) and **input-modality divergence** (27 findings), now rules 7–8. This PR distills those clusters into eight imperative rules in AGENTS.md so agents apply them **before writing code**, adds one client-consumption invariant to ARCHITECTURE.md §5, and places the test-quality rule in TESTING.md (per the team decision that testing docs are the canonical guide for review standards), cross-referenced from AGENTS.md. Each rule cites the PRs where it was litigated. Raw mining data: `reviews.jsonl` / `comments.jsonl` + `backfill/buzz-review-findings.jsonl` (review-mining artifacts, not committed). No code changes. CLAUDE.md is a symlink to AGENTS.md and picks this up automatically. 🤖 Drafted by Jude's agent from automated mining of this repo's last 25 PRs' review threads and 71 agent-review rooms; every rule cites the PRs where it was litigated. Jude reviews and owns the result. Mining method + raw cluster data available on request. --------- Signed-off-by: Jude Edwards <judeedwards@squareup.com>
…n-surface * origin/main: docs: add review-proven failure-path & async-state rules to AGENTS.md (#7061) fix(desktop): back split thread headers (#7137) add public descriptions to agent personas (#7126) feat(desktop): add protected-build Bestie experiment (#6902) fix(relay): reject a frame on its own acknowledgement channel (#6961) fix(acp): wake agents from workflow messages (#6953) feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904) perf(mobile): reduce cold startup and channel rendering delays (#6996) feat(mobile): push notifications MVP (#6269) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
* fix: retrieving cold memories; add regression task (#6950)
## Why
Evaluating buzz agent memory retrieval by seeding a memory then asking
the buzz agent a question it needs that memory.
**Bug Found**: System prompt had no inclusion of retrieving cold
memories and suggested looking in a mem/*.md directory that does not
exist. Updated `system-prompt.md` to include memory CLI tools and usage.
Eval Before System Prompt Change: 0/3
Eval After System Prompt Change: 3/3
## What
- Add a `memory-retrieval` benchmark that seeds agent memory with `buzz
mem set` before asking a direct question.
- Grade the observable threaded answer without inspecting tool calls or
exposing the answer in channel history.
- Teach agents to use `buzz mem set`, `buzz mem ls`, and `buzz mem get`
for cold memory.
- Add a wire-debug endpoint configuration for diagnosing ACP tool calls
in local runs.
- Add fixture, seeding, verifier, and prompt coverage.
## Risk Assessment
Low. The runtime changes are limited to the benchmark harness. The
production-facing change clarifies existing memory commands in the base
prompt; it does not change memory storage, relay behavior, or
authorization.
## References
- Before the system-prompt changes, 0/3 attempts passed because agents
never invoked the `buzz mem` CLI and instead searched a non existent
filesystem
- After the changes, 3/3 attempts passed. ACP wire logs confirmed that
every agent ran `buzz mem ls` followed by `buzz mem get` and returned
`net_gpv`.
---------
Signed-off-by: Philip Azar <pazar@squareup.com>
* fix(ci): salvage Codex review output on PTY-shutdown hang (#7042)
Codex CLI can leave a PTY descendant holding the action's inherited
stdio after the turn completes. The `runCodexExec.ts` wrapper waits on a
`close` event that never fires, so the `Review pull request` step hangs
until the job timeout kills it — discarding the finished review the CLI
already wrote to disk.
The CLI writes the completed review to the `--output-last-message` file
(exposed as `output-file`) **before** the hang. This PR adds a salvage
step that recovers it, and sets the step and job timeouts to preserve
the full 30-minute Codex execution budget.
**Changes (`codex-security-review.yml`):**
- Add `output-file: ${{ runner.temp }}/codex-review.json` to the `Review
pull request` step so the CLI writes the result before the hang.
(`runner` context is valid in `steps.with`; not in `jobs.env`.)
- Add `timeout-minutes: 30` and `continue-on-error: true` to the Codex
step — a hang now costs ≤30 minutes instead of 40, and the salvage step
still runs.
- Set job `timeout-minutes: 40` to give setup, step cancellation, and
salvage sufficient headroom without colliding with the Codex execution
budget. The original 30-minute job timeout was too narrow: evidence from
run
[33114428326](https://github.com/block/buzz/actions/runs/33114428326/job/98665369165)
shows completed output appearing 28m46s after step start, meaning a
20-minute step timeout could kill a legitimate review before the salvage
file exists.
- Add a `Salvage review output` step with `if: always()`: prefers
`steps.run_codex.outputs.final-message` on a clean exit; falls back to
the output file when the step timed out. The output file path is set in
the step's own `env` block (`CODEX_OUTPUT_FILE: ${{ runner.temp
}}/codex-review.json`), where `runner` is valid. Validates shape
(non-empty JSON object, has `overall_risk`); fails the job hard if
neither source is present.
- Wire the job `outputs.review_json` to
`steps.salvage.outputs.review_json`.
**Changes (`Justfile`, `ci.yml`):**
- Add `actionlint .github/workflows/codex-security-review.yml` to
`security-review-check` so expression-validity errors are caught
locally.
- Provision `actionlint` via Hermit (pinned v1.7.12) rather than a
one-off `Install actionlint` curl step, so the same binary is used
locally and in CI.
**Security posture is unchanged:** the salvage step reads the action's
own output and a file written to `runner.temp` — neither is
PR-controlled. Credential-stripping env block on the Codex step is
untouched.
Note this is a temporary workaround until
https://github.com/openai/codex-action/issues/169 is addressed
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
* feat: render agent avatars as squircles (#7106)
## Summary
- render every agent/AI identity as a 30% squircle across desktop and
mobile while keeping human avatars circular
- propagate agent identity through message, thread, profile, reaction,
member, DM, search, workflow, project, huddle, forum, pulse, and
agent-management surfaces
- preserve squircle geometry for fallbacks, focus/status treatments,
add-agent controls, and overlapping avatar outlines (`calc(30% + 2px)`
for the outer background)
### Related issue
None found. This change was requested and visually reviewed in the
originating Buzz thread.
### Testing
- `just desktop-test` — 5,799 passed
- `just mobile-test` — 2,008 passed
- pre-push gates passed at `0d59d77b120dcb90aac2f918e422c11c9fa5353b`:
desktop check, TypeScript typecheck, desktop full test suite, mobile
format/analyze and full test suite, Rust tests, Tauri checks, and
differential file-size gate
- deterministic desktop visual sweep covered channel messages/thread
summaries; thread, subthread, and sub-subthread depths; reactions and
reactor popovers; hover/full profiles; added-to-channel activity;
channel members/settings; agent library/team overlaps; agent creation;
mention autocomplete; and DM header/sidebar/settings
### UI evidence
The complete labeled visual matrix is available in the originating Buzz
review thread. GitHub-hosted copies will be added in a follow-up PR
comment using the repository screenshot script.
---------
Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
* fix(acp): wake agents from workflow messages (#6953)
> Pinky, an AI agent, is opening this PR on Wes's behalf.
## Summary
Workflow-generated messages can contain a valid agent mention but still
fail the ACP inbound author gate because the relay signs the event. This
keeps the existing wake policy and gives ACP a narrowly verified
effective author:
- preserve the workflow owner's existing `p` tag and all
rendered-mention `p` tags
- add explicit `["buzz:workflow-owner", <owner hex>]` provenance to
relay-generated workflow messages
- add `["buzz:workflow-mention", <agent hex>]` authority only for
mentions resolved from the stored, unrendered workflow step template
- accept that owner only for a verified kind-9 event signed by the
relay's current NIP-11 `self` key, with unique canonical workflow
metadata and an explicit workflow mention for the receiving agent
- route the verified owner through the existing author and in-flight
mode policies in both normal and setup listeners
- refresh relay identity after reconnects, retaining the last verified
key on transient fetch errors while treating a successful response
without `self` as definitive removal
Malformed, duplicate, forged, tampered, wrong-kind, and wrong-relay
attribution all fail closed to the raw event signer. `respond-to=nobody`
remains absolute. Old/mixed-version messages without the explicit
provenance retain their current fail-closed behavior.
## Trust boundary
The workflow owner means **“scheduled by,” not “authored every rendered
word.”** Trigger-controlled substitutions may still produce ordinary `p`
mention routing for compatibility, but they cannot mint
`buzz:workflow-mention` authority. Only a target named in the durable
owner-authored step template can receive that authority.
The author gate is not bypassed: after relay signature/provenance
verification, the effective owner is evaluated under the same
`owner-only`, `allowlist`, DM, and `nobody` policies used for ordinary
messages. Owner control commands continue to use the raw event signer.
## Why this PR
This is the focused immediate fix for waking an **online** agent from a
stored workflow mention. Earlier attempts were not a finished mergeable
fix and had materially different or incomplete trust designs. Larry's
larger draft stack addresses durable delivery across restarts; that
remains valuable future work and can supersede this effective-author
path when it lands.
## Validation
At exact clean commit `fe5b55619fe44176343eefb4cb7fe180df45a7d8`:
- `buzz-relay workflow_sink`: 25/25 passed, including all four ignored
PostgreSQL cases
- `buzz-acp --lib`: 845/845 passed
- `buzz-workflow --lib`: 169/169 passed (2 unrelated PostgreSQL tests
ignored)
- warnings-denied Clippy passed for the changed Rust packages
- `cargo fmt --all -- --check` passed
- `git diff --check` passed
- repository pre-push gates passed, including branch-scoped Rust tests
- CI now selects the ACP library tests and the relay's pure + PostgreSQL
workflow-sink tests so these guards cannot silently remain unexecuted
The production event-to-author gate is shared by normal and setup
listeners and has biting regression tests for accepted explicit
attribution, legacy owner-`p` rejection, and forged-attribution
rejection.
## Exact-head local relay + ACP proof
Following the release-binary/local-relay shape in `TESTING.md`, the
exact commit above passed a fresh isolated real-process matrix using:
- a freshly recreated Postgres database with migrations
- isolated Redis
- exact-head release `buzz-relay`, `buzz`, `buzz-admin`, and `buzz-acp`
binaries
- newly provisioned owner, channel, and bot member through the CLI
- workflow creation and triggering through the running relay
- a deterministic ACP protocol subprocess capturing actual
`session/prompt` dispatches
- a NIP-11 `self` value verified against the running relay signer
Cases:
1. A stored explicit workflow mention woke an `owner-only` agent exactly
once.
2. A workflow message without an agent mention did not wake it.
3. A non-relay signer forging every workflow authority tag did not wake
it.
4. Trigger-controlled `{{trigger.text}}` containing `@Wake Agent`
retained ordinary `p` routing but received no authority-bearing
workflow-mention tag and did not wake the agent.
5. `respond-to=nobody` remained absolute for a valid relay-authenticated
workflow mention.
The deterministic ACP subprocess isolates and directly proves relay →
ACP authorization and prompt dispatch without depending on external
model behavior.
## Deployment and residual risk
Relay and ACP changes must be deployed together for the new wake
behavior; mixed versions fail closed. Production paired-deployment proof
remains distinct from the successful local integration run. Setup-mode
behavior has automated coverage but was not a separate case in the
five-case local matrix. Relay-key rotation is observed at ACP
startup/reconnect; transient NIP-11 errors retain the last verified key,
an intentional availability tradeoff documented in code.
---------
Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: LioLionel <62820906+LioLionel@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
* fix(relay): reject a frame on its own acknowledgement channel (#6961)
Pinky, an AI agent, updated this description on Wes's behalf after
taking over the startup investigation.
**Category:** fix
**User Impact:** An EVENT refused by WebSocket admission or handler
saturation receives a correlated `OK(event_id, false, reason)` instead
of an uncorrelated NOTICE, so the client can settle that refusal without
waiting for its publish timeout. Rate-limited refusals also arm client
backoff. This fixes a protocol failure mechanism; it does not establish
that every startup send will succeed or that the reported Desktop
startup incident is fully resolved.
**Problem:** Startup opens several live subscriptions and publishes at
once, and the relay's WebSocket admission gate is a fixed 5-second
window (`ws_admission_budget` = `human_ws_events_per_sec * 5`). If that
shared per-principal quota is exhausted, `enforce_ws_admission`
previously rejected an EVENT with a bare `["NOTICE", reason]`. Quota
pressure is a possible trigger, not proof of the original incident's
complete cause.
A NOTICE carries no event id. Both clients settle a pending publish
*only* from an `OK` keyed by event id (desktop `pendingEvents`, mobile
`_pendingEvents`), so nothing settled — and `handle_text_message`
returns early, so no `OK` ever followed either. The send **could not
fail**; it could only time out at `PUBLISH_TIMEOUT_MS` = 25s. That
explains how this rejection mechanism can produce a roughly 25-second
timeout; attributing the original report to it still requires the actual
startup/send workflow.
The handler-semaphore saturation path had the identical defect, and that
one needs no quota burst to fire.
**Solution:** NIP-01 gives each request type its own acknowledgement
channel, and a rejection is only actionable on the same one. Reject a
REQ with `CLOSED`, an EVENT with `OK(id, false, reason)`, and fall back
to `NOTICE` only where no per-request correlation exists. COUNT refusals
now also use `CLOSED(query_id, reason)` per NIP-45, covering both quota
admission and handler saturation (added in
`cd12c93804b87a24b61075dfd171dc471a0a527f`).
Reason strings are unchanged, so the `rate-limited:` prefix and `retry
in {N}s` hint that existing client gates parse keep working (desktop
`parseRateLimitHint`, mobile `RelayRateLimitGate`, buzz-acp
`set_rate_limit_gate`). Only the frame *type* changes, so
`docs/multi-tenant-relay.md` L7 stays satisfied.
Two notes on how this landed, both worth a reviewer's attention:
1. **A survived mutation became a design change.**
`send_admission_result` originally took a `RejectionTarget` parameter,
and reverting the *second* call site (the per-minute message quota)
survived the whole suite — with Redis unreachable the first quota check
short-circuits, so that line is unreachable in test. Rather than test
around it, the parameter is gone: the target is derived from the frame,
so no call site can name the wrong channel.
2. **The relay fix would have caused a client regression on its own.**
Gate arming lived only in the NOTICE branch. Once rejections arrive as
`OK:false`, `handleOk` failed the send without ever backing off — the
client would retry straight into the same quota. Desktop and Mobile now
arm on a `rate-limited:` OK rejection. ACP was subsequently fixed in
`3b06dd32493596ec650f20abf8805791c50fdc24`: it arms the gate and
re-parks only the refused observer frame, preserving other in-flight
frames. Desktop gets `activateRateLimitIfSignalled` as the single owner
of that prefix test, called from both `handleOk` and the NOTICE branch.
<details>
<summary>File changes</summary>
**crates/buzz-relay/src/rejection.rs** (new)
Owns the admission-rejection concern: `RejectionTarget`,
`rejection_target_for`, `request_rejection_message`,
`send_admission_result`, and `enforce_ws_admission`, moved out of
`connection.rs`. Six tests, two of which drive the real
`enforce_ws_admission` against a real `AppState`.
**crates/buzz-relay/src/connection.rs**
Fix the EVENT handler-semaphore rejection to correlate to the event id;
delegate admission to the new module. Add two tests that drive the real
`handle_text_message` with every handler permit held. Down from 1319 to
1116 lines.
**crates/buzz-relay/src/state.rs**
Widen the existing `test_state` helper to `pub(crate)` so the rejection
tests reuse it rather than adding a ninth copy of `AppState`
construction.
**desktop/src/shared/api/relayRateLimitGate.ts**
Add `activateRateLimitIfSignalled` — one owner for the `rate-limited:`
prefix test, since three inbound frame types now carry it.
**desktop/src/shared/api/relayClientSession.ts**
Arm the gate on a rate-limited OK rejection; route the NOTICE branch
through the same helper. Net zero lines, which keeps this
already-oversized file within the differential ratchet.
**desktop/src/shared/api/relayClientPublishRejection.test.mjs** (new)
Four tests against the real `RelayClient`: a rate-limited OK settles the
pending publish and arms the gate; an ordinary rejection does not arm
it; an accepted OK still resolves.
**mobile/lib/shared/relay/relay_session.dart**
Arm the gate in `_handleOk` for a rate-limited rejection.
**mobile/test/shared/relay/relay_session_test.dart**
Two tests driving the real `publish` + `debugHandleMessage` path.
</details>
<details>
<summary>Validation</summary>
**Mutation-tested — 5 mutations, all now killed.** Each production call
site was reverted to the defective behaviour to confirm a test fails.
This caught two false-negative tests:
| # | Mutation | Result |
|---|----------|--------|
| 1 | `rejection_target_for`: EVENT → `Connection` | 4 tests fail |
| 2 | EVENT handler-semaphore call site → bare NOTICE | **survived at
first** |
| 3 | per-minute quota call site → `Connection` | **survived**; fixed by
removing the parameter |
| 4 | desktop `handleOk` gate arming removed | 1 test fails |
| 5 | mobile `_handleOk` gate arming removed | 1 test fails |
Mutation 2 is the lesson: my first saturation test called
`request_rejection_message` directly, so reverting the real call site
inside the `match` arm left it green. It now drives
`handle_text_message` itself and dies on that mutation.
- `cargo test -p buzz-relay` — 928 passed, 1 failed:
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo`,
**pre-existing**, reproduced with all changes stashed at `4dd4d73de`.
- `cd desktop && npm test` — 5721 passed, 0 failed (full suite).
- `cd mobile && flutter test` — 1876 passed, 0 failed (full suite).
- `just fmt-check`, `just clippy`, `just desktop-check`, `just
mobile-check`, `just file-size-check` — clean. Desktop's 5 biome
warnings are pre-existing (reproduced with changes stashed).
- All 9 pre-push lanes green, including `rust-tests` and
`desktop-tauri-checks`.
**Not verified:** not reproduced end-to-end against a live relay under a
forced quota burst. The causal chain is source-proven and
mutation-proven at the frame level; the ~25s attribution follows from
`PUBLISH_TIMEOUT_MS` but is not directly measured. A packaged-build
click-through would close that gap.
</details>
Related work: #6957 bounds Desktop HTTP event submission, but safe
retained-operation recovery after exhausted/ambiguous outcomes remains
unfinished. #6998 is the separately reviewable Desktop
readiness/duplicate-subscription slice. Neither is claimed to complete
native before/after startup-send validation.
Diagnosis note: `RESEARCH/DESKTOP_STARTUP_SEND_STALL_2026_08_27.md`
(Brain's workspace).
## Current review disposition (2026-08-28)
The [review on
`cd12c938`](https://github.com/block/buzz/pull/6961#pullrequestreview-5052902510)
identified ACP's missing rate-limited-OK handling. Commit
`3b06dd32493596ec650f20abf8805791c50fdc24` fixes gate arming, re-parking
the specifically refused observer frame, and the stale NOTICE comment.
Two regressions drive the real frame dispatcher. See [the implementation
and validation
response](https://github.com/block/buzz/pull/6961#issuecomment-5455032054).
The Mobile generation-check inline thread is resolved: its `async
publish` returns a failed Future when superseded; it does not throw
synchronously at invocation. No further production change was indicated
by that comment.
The validation counts above describe the original slice, not a new
rerun. At `3b06dd324`, the current GitHub check rollup has successful
completed test/build checks (non-applicable jobs skipped). The
security-review comment still requires review for the current base/head
range; do not read a green authorization job as a completed security
review. Approval and merge remain human decisions.
---------
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
* feat(desktop): add protected-build Bestie experiment (#6902)
## Summary
Introduces a protected-build boundary for the default-off Bestie
experiment without adding any Bestie product surface.
- Official OSS builds select an empty protected-feature module and emit
no Bestie/Chief metadata or implementation content.
- Protected internal builds select a separate module graph containing
the Bestie experiment definition.
- Within an internal build, Bestie remains disabled until the user opts
in under Settings → Experiments.
- The production build runs an artifact matrix and fails if OSS output
contains protected content or internal output lacks the Bestie manifest.
## Build contract
| Build variant | User opt-in | Result |
| --- | --- | --- |
| Official OSS | Any/forged | Bestie absent from the compiled artifact |
| Protected internal | Off | Bestie available but disabled |
| Protected internal | On | Bestie enabled |
The companion protected-release change is squareup/buzz-releases#91. It
sets `VITE_BUZZ_BESTIE=1`, requires that exact value, forwards it into
the signed macOS build, and asserts the contract in release validation.
## Why this is separate
This gives later Bestie PRs one build-selected import seam. Protected
implementations must be reachable only from the internal module so they
never enter the official OSS module graph.
## Non-goals
- No Bestie persona or provisioning
- No sidebar, app-chrome, or message-toolbar UI
- No entitlement or secrecy claim: the source is public; this boundary
controls official Block artifacts
## Verification
- Exact commit `523cf49ced03cba9be43836a54d6aa5d6923cc82`
- Full `just ci`: 5,673 Desktop tests, 2,773 Tauri tests, 1,860 mobile
tests, Rust/Tauri/web/mobile static checks and builds
- OSS production artifact: scanner confirms no `Bestie`, `Chief of
Staff`, or `builtin:bestie` content
- Internal production artifact: scanner confirms the protected Bestie
manifest is emitted
- Both build orders verified; `dist` retains the requested variant for
Vite/Tauri packaging
---------
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Signed-off-by: Fizz <fizz@buzz.local>
Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Fizz <fizz@buzz.local>
Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
* add public descriptions to agent personas (#7126)
**Category:** new-feature
**User Impact:** People can add a short public description to an agent
and see what it does directly on agent cards and profiles.
**Problem:** Agent cards previously showed only a model label, so people
had to open an agent and inspect its instructions to understand its
purpose. Public metadata also needed one trustworthy lifecycle across
local edits, relay catalogs, profiles, and portable snapshots.
**Solution:** Add an optional owner-authored description with a
280-character visible-text policy, publish it as profile `about`, and
prefer it on agent cards while retaining the model fallback. Description
metadata is excluded from the spawn-content hash, remains
definition-owned, and is validated independently at every untrusted or
persistence boundary.
<details>
<summary>File changes</summary>
**desktop/src-tauri/src/commands/agent_config_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/agent_discovery/relay_directory.rs**
Updates relay-directory profile test publication for the expanded
profile contract.
**desktop/src-tauri/src/commands/agent_models_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/agent_models_update.rs**
Preserves the effective `about` value when instance edits republish a
complete profile event.
**desktop/src-tauri/src/commands/agents.rs**
Carries the effective authored description into initial managed-agent
profile publication.
**desktop/src-tauri/src/commands/agents_profile.rs**
Adds `about` to profile reconciliation and keeps description, name, and
avatar synchronized against relay state.
**desktop/src-tauri/src/commands/agents_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/personas/card.rs**
Materializes the definition-owned description before minting a portable
agent card snapshot.
**desktop/src-tauri/src/commands/personas/create.rs**
Normalizes and validates raw authored descriptions before persona
persistence.
**desktop/src-tauri/src/commands/personas/delete_cascade_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/personas/inbound.rs**
Validates descriptions at inbound relay ingress and applies accepted
values to local definitions.
**desktop/src-tauri/src/commands/personas/inbound/catalog_reconcile_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/personas/inbound/inbound_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/personas/mod.rs**
Centralizes raw-byte validation followed by trim/empty normalization for
description writes.
**desktop/src-tauri/src/commands/personas/pending.rs**
Revalidates descriptions before preparing public persona publications.
**desktop/src-tauri/src/commands/personas/sharing.rs**
Carries the optional public description through this managed-agent
compatibility path.
**desktop/src-tauri/src/commands/personas/snapshot.rs**
Materializes definition-owned descriptions into portable instance
snapshots without creating a second persisted authority.
**desktop/src-tauri/src/commands/personas/snapshot/fidelity_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/personas/snapshot/import.rs**
Restores snapshot descriptions onto imported definitions while keeping
linked instance copies absent.
**desktop/src-tauri/src/commands/personas/snapshot/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/personas/update.rs**
Persists persona description edits, republishes linked profiles, and
preserves legacy avatars during complete kind:0 replacements.
**desktop/src-tauri/src/commands/personas/update/name_propagation_tests.rs**
Proves description-only profile sync does not write instance state or
clear a legacy avatar.
**desktop/src-tauri/src/commands/team_snapshot.rs**
Round-trips member descriptions through team snapshots and imported
definitions.
**desktop/src-tauri/src/commands/team_snapshot/tests.rs**
Covers team member description export and import fidelity.
**desktop/src-tauri/src/commands/teams/adopt/apply.rs**
Starts adopted team catalog members without synthesizing an unauthored
description.
**desktop/src-tauri/src/commands/teams/adopt/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/teams/pending/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/commands/teams/sharing/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/egress_guard_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/event_sync_team_catalog_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/agent_description.rs**
Defines the canonical Rust description resolution used by profile
publication and reconciliation.
**desktop/src-tauri/src/managed_agents/agent_events.rs**
Updates managed-agent record construction for the optional public
description field.
**desktop/src-tauri/src/managed_agents/agent_snapshot.rs**
Includes descriptions as snapshot profile `about` metadata and validates
them at decode ingress.
**desktop/src-tauri/src/managed_agents/agent_snapshot_envelope.rs**
Updates managed-agent record construction for the optional public
description field.
**desktop/src-tauri/src/managed_agents/agent_snapshot_tests.rs**
Covers snapshot description export and rejection of unsafe or overlong
imported metadata.
**desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/definition_validation.rs**
Adds the shared 280-character visible-text policy for public
descriptions.
**desktop/src-tauri/src/managed_agents/discovery/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/effective_config/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/global_config/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/mod.rs**
Exports the description resolution and validation helpers to
managed-agent consumers.
**desktop/src-tauri/src/managed_agents/nest/render_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/parallelism.rs**
Updates managed-agent fixtures for the optional description field
without changing runtime configuration behavior.
**desktop/src-tauri/src/managed_agents/persona_events.rs**
Adds description to persona event content while deliberately excluding
it from the spawn-relevant content hash.
**desktop/src-tauri/src/managed_agents/persona_events/tests.rs**
Pins description event round-tripping and proves description-only edits
do not change the restart hash.
**desktop/src-tauri/src/managed_agents/personas.rs**
Initializes built-in persona records without authored descriptions for
backward-compatible defaults.
**desktop/src-tauri/src/managed_agents/personas/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/readiness.rs**
Updates managed-agent fixtures for the optional description field
without changing runtime configuration behavior.
**desktop/src-tauri/src/managed_agents/restore.rs**
Includes the effective description in launch-time profile
reconciliation.
**desktop/src-tauri/src/managed_agents/runtime/test_fixtures.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/runtime/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/spawn_snapshot/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/team_catalog/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/team_snapshot.rs**
Updates managed-agent record construction for the optional public
description field.
**desktop/src-tauri/src/managed_agents/teams_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/managed_agents/types.rs**
Adds optional description metadata to persona and managed-agent records
and their compatibility projections.
**desktop/src-tauri/src/managed_agents/types/requests.rs**
Accepts optional descriptions on persona create and update IPC requests.
**desktop/src-tauri/src/managed_agents/types/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/migration_avatar_tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src-tauri/src/persona_catalog.rs**
Parses and validates descriptions at the untrusted community-catalog
boundary.
**desktop/src-tauri/src/persona_catalog_tests.rs**
Covers valid catalog descriptions plus rejection of malformed,
invisible, and overlong values.
**desktop/src-tauri/src/relay.rs**
Publishes and queries kind:0 `about` so relay profiles preserve authored
descriptions.
**desktop/src-tauri/src/relay/tests.rs**
Updates managed-agent/persona fixtures for the optional description
field while preserving the behavior under test.
**desktop/src/features/agents/AGENTS.md**
Documents description ownership, validation, snapshot, hashing, and
display invariants for future changes.
**desktop/src/features/agents/lib/agentDescription.test.mjs**
Pins Unicode counting, paste clamping, trimming, and empty
authored-description behavior.
**desktop/src/features/agents/lib/agentDescription.ts**
Provides shared display resolution, Unicode-scalar counting, and paste
clamping for descriptions.
**desktop/src/features/agents/lib/personaCatalogRelay.ts**
Maps validated catalog descriptions into catalog persona projections.
**desktop/src/features/agents/ui/AgentDefinitionDialog.tsx**
Adds the description draft to create and edit submission while
extracting identity fields from the large dialog.
**desktop/src/features/agents/ui/AgentDescriptionField.tsx**
Renders the public description input, helper copy, and Unicode-aware
near-limit counter.
**desktop/src/features/agents/ui/AgentIdentityCard.tsx**
Generalizes the card second line to show a two-line description or the
existing model fallback.
**desktop/src/features/agents/ui/UnifiedAgentsSection.tsx**
Prefers authored descriptions on persona cards and retains model labels
when no description exists.
**desktop/src/features/agents/ui/personaDialogState.test.mjs**
Verifies edit and duplicate drafts preserve authored descriptions.
**desktop/src/features/agents/ui/personaDialogState.ts**
Seeds authored descriptions into edit and duplicate dialog drafts.
**desktop/src/features/agents/ui/usePersonaActions.ts**
Preserves descriptions when copying catalog personas into local
definitions.
**desktop/src/shared/api/personaTypes.ts**
Defines description-bearing persona wire types in a focused module split
from the size-constrained API type file.
**desktop/src/shared/api/tauriPersonas.test.mjs**
Verifies raw persona descriptions map into the frontend model and absent
values become null.
**desktop/src/shared/api/tauriPersonas.ts**
Maps description fields across Tauri and preserves raw authored bytes
for authoritative Rust validation.
**desktop/src/shared/api/types.ts**
Re-exports the extracted persona types without changing consumer import
paths.
**desktop/src/testing/e2eBridge.ts**
Extends mock persona create, update, publication, and catalog parsing
with production-shaped description behavior.
**desktop/tests/e2e/agents.spec.ts**
Verifies an edited description persists and appears on the agent card.
</details>
### Reproduction Steps
1. Open **Agents**, edit a custom or built-in agent, and enter a
sentence in **Description**.
2. Save the agent and confirm the sentence appears as the second line on
its card.
3. Reopen the agent and confirm the authored description is restored;
clear it and confirm the card returns to the model label.
4. Paste more than 280 Unicode characters and confirm the field keeps
the first 280 characters and shows the near-limit counter.
5. Share or export/import the agent and confirm the description survives
in the catalog/profile or snapshot without showing a restart-required
badge for a description-only edit.
### Screenshots / Demo
The focused Playwright flow `built-in persona edits persist` exercises
the edited dialog, persisted value, and resulting card subtitle.
Screenshots can be added after review if the field placement or two-line
card treatment needs visual iteration.
### Verification
- `cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib` —
3,029 passed
- `cd desktop && pnpm test` — 5,805 passed
- `cd desktop && pnpm exec tsc --noEmit`
- Focused Playwright: `built-in persona edits persist` — passed
- Pre-push desktop, Tauri, typecheck, test, file-size, and branch-skew
gates — passed
---------
Signed-off-by: tulsi <tulsi@block.xyz>
* fix(desktop): back split thread headers (#7137)
## Summary
- render an auxiliary panel's requested header backdrop in docked/split
mode
- preserve explicit transparent-backdrop behavior
- cover a populated, scrolled thread pane so timeline content cannot
bleed through its header
## Root cause
`RightAuxiliaryPane` correctly paints above the channel's shared header
backdrop so close/edit controls remain visible. The docked
`AuxiliaryPanelHeader` branch, however, ignored its `backdrop` request,
leaving scrolled thread content in that higher stacking context
unbacked.
## Verification
- desktop unit suite: 5,801 passed
- desktop TypeScript: passed
- Biome checks: passed (existing unrelated repository warnings only in
the earlier full run)
- targeted Playwright scroll regression: passed
- ultrawide thread-pane Playwright coverage: passed
Signed-off-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
Co-authored-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
* docs: add review-proven failure-path & async-state rules to AGENTS.md (#7061)
Mining the last 25 PRs' review threads (45 substantive findings, 11
reviewed PRs, avg **4.8 review rounds** each) shows **53% of findings
are repeats** of five clusters: swallowed failures, stale-async-state
races, tests that don't bind the production seam, unbounded
resources/retry loops, and non-atomic multi-step persistence. PR #6956
alone burned 4 rounds converging on one of these classes.
A second, independent mining pass over **71 agent-review rooms (303
findings, Aug 18–29)** confirmed the same clusters and added outcome
data — how often authors actually fix each finding class once flagged:
test-seam binding and unbounded-resource findings **100%**, swallowed
errors **90%**, stale-state races **70%**. It also surfaced two clusters
the GitHub-thread pass under-sampled: **assistive-semantics defects**
(44 findings, second-largest cluster) and **input-modality divergence**
(27 findings), now rules 7–8.
This PR distills those clusters into eight imperative rules in AGENTS.md
so agents apply them **before writing code**, adds one
client-consumption invariant to ARCHITECTURE.md §5, and places the
test-quality rule in TESTING.md (per the team decision that testing docs
are the canonical guide for review standards), cross-referenced from
AGENTS.md. Each rule cites the PRs where it was litigated. Raw mining
data: `reviews.jsonl` / `comments.jsonl` +
`backfill/buzz-review-findings.jsonl` (review-mining artifacts, not
committed).
No code changes. CLAUDE.md is a symlink to AGENTS.md and picks this up
automatically.
🤖 Drafted by Jude's agent from automated mining of this repo's last 25
PRs' review threads and 71 agent-review rooms; every rule cites the PRs
where it was litigated. Jude reviews and owns the result. Mining method
+ raw cluster data available on request.
---------
Signed-off-by: Jude Edwards <judeedwards@squareup.com>
* feat(buzz-acp): give each channel thread its own agent session (#6732)
## What this does
In a channel, people often run several unrelated conversations at once
(separate threads). Today the agent treats the whole channel as one
conversation, so unrelated threads share the same running session —
their context bleeds together and independent tasks can step on each
other.
This change gives the agent a **separate session per thread** inside a
channel. Direct messages stay as one conversation (unchanged). The
channel is still the boundary for who is allowed in and what is visible
— only the agent's working context is now split by thread.
## How it is turned on
Off by default. Operators opt in with one setting:
- `BUZZ_ACP_SESSION_POLICY=channel` — default, current behavior
- `BUZZ_ACP_SESSION_POLICY=thread` — new per-thread behavior
Being behind a flag means we can enable it for a few agents, watch how
it behaves, and roll back instantly without a code change.
## Key design decisions
- **Decide the thread once, up front.** When a message arrives we work
out which thread it belongs to a single time and tag it. Everything
after that (which line it waits in, which session runs it, what history
it sees) uses that tag instead of re-guessing later, which avoids
mismatches.
- **Default stays identical to today.** Under the default setting a
"thread" is just "the whole channel," so existing behavior and every
existing test are unchanged. The new, riskier behavior is strictly
opt-in.
- **Give the agent only its thread's history.** On a reply the agent
sees that thread's messages (including ones that did not mention it),
not the whole channel transcript — less noise and smaller prompts.
- **Don't let one channel use more memory than before.** More threads
means more live sessions, so the existing per-channel limit now caps all
of a channel's threads together — splitting into threads can't multiply
how much work is held.
## Bugs found and fixed while iterating (from review)
- **Same thread, two sessions.** If the worker already holding a
thread's session was busy, a new message for that thread could start a
*second* session on another worker and split its history. Now it waits
for the right worker instead of forking.
- **Interrupting the wrong thread.** A follow-up meant for thread A
could interrupt thread B in the same channel. Interrupts now target the
exact thread.
- **Stuck thread after a crash.** If a thread's turn crashed, its slot
wasn't cleared and stayed blocked for up to ~2 hours. It now clears
right away and retries.
- **Lost the original request.** When a thread was interrupted and then
had to wait for a busy worker, only the follow-up was kept and the
original request was dropped. The full request is now preserved on
retry.
- **Same thread seen as two.** Two spellings of the same thread id
(upper/lower case) could be treated as different threads. Normalized so
they count as one.
## Not in this PR
- The desktop Settings toggle and rollout wiring for managed agents —
https://github.com/block/buzz/pull/6909
- One pre-existing retry edge case (present today without this flag,
unrelated to this change) — tracked separately so this PR stays focused.
## Testing
The full `buzz-acp` test suite passes (830+ unit and integration tests),
plus new focused tests for thread routing, session reuse, interrupt
targeting, crash recovery, and request preservation. Behavior with the
flag off is unchanged.
---------
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Signed-off-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
Co-authored-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
* feat(db): add NIP-FI identity and final-admission schema foundation (#6994)
PR 2 of the NIP-FI plan: the schema foundation. Establishes the durable
server-side identity ledger and final-admission surface that the runtime
phases build on. All of Phase A's migrations live here; later phases own
their own deltas.
Depends on nothing — PR 1 (#6776, merged) owned zero migration files.
This PR's relations are shaped to store exactly what PR 1's verifier
produces: issuer-qualified identity and the four denial classes. They
meet in a later PR that writes a verified assertion into these tables in
one transaction.
## Two internally-ordered migrations
- `0041_nip_fi_identity_foundation.sql` (migration A) — core identity +
base-lifecycle relations (5 tables): issuer-qualified `(iss, sub)`
bindings, lifecycle history/selectors, enrollment policies, and
operation receipts. Applies cleanly to current `main`.
- `0042_nip_fi_authorization_foundation.sql` (migration B) — the
final-admission surface (10 tables): authorization events + capacity,
admission results, replay/receipt guards, audit, invalidation
domains/floors, protected-object authority, authority epochs, and
restore version deltas. Applies to A's resulting state.
Fifteen NIP-FI relations total, zero dangling foreign keys. Identity is
issuer-qualified throughout — no single-global-issuer assumption in any
relation, no `Block`-hardcoding. A single deployment may run one issuer;
that is config, not schema.
## Durable, immutable ledger posture
All 15 relations are append-only (immutable `no_delete`/`no_truncate`
triggers) and carry `community_id` as provenance, not ownership. Both
migrations widen the single SQL source of truth
`community_write_fence_excluded_table` so the relations are never
fence-attached, never purged on community deletion, and never counted as
tenant-scoped drift by the deletion control plane's exact-set catalog
check — the same posture main already applies to `product_feedback` and
`rate_limit_violations`. `schema/schema.sql` keeps one consolidated
definition of that function whose exclusion array byte-matches `0042`,
guarded by a parity assertion so a future consolidation cannot silently
drop NIP-FI relations from the ledger.
This makes a tenant's identity/authorization ledger survive community
deletion, per the spec's `FI-INV-02` (durable binding) and `FI-INV-03`
(tombstone monotonicity) and `NIP-FI.md`'s "durable server state"
ruling. `communities(id)` FK never dangles: community rows become
permanent tombstones, never hard-deleted.
## Authorization shape and cardinality contracts
Authenticated `OperatorDenied` events (`actor_kind` 1–3, non-null
`request_fingerprint`) carry a null `semantic_fingerprint` and commit
without a denial-attempt row. The denial-attempt cardinality and shape
guards are scoped to unresolved pre-auth kind-9 events (`actor_kind =
4`). Applied and no-op lifecycle receipts (`outcome_code IN (1, 3)`)
require exactly one mapped success-transition event; denied lifecycle
receipts (`outcome_code = 2`) require zero events from the complete core
lifecycle success-transition class (kinds 1, 2, 3, 6: enrolled, revoked,
rotated, retired) — any such event paired with a denied receipt would
record a transition that never occurred.
## Mined vs. new
Re-cut from Franco's #1476 (`0029`/`0030`) and Cea's #4772 committer
schema, re-cut along FK topology and renumbered above the live `main`
tip. The buzz-auth core of #1476 is Cea-authored; `Co-authored-by`
reflects verified per-commit authorship of the mined schema.
Zero Rust/`deletion.rs` edits — the migration-only exclusion widening
keeps `EXPECTED_SCOPED_TABLES` untouched.
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
* fix(model-capabilities): humanize databricks goose model names (#7135)
🤖
## Summary
- add curated human-readable labels for Databricks Goose models that
otherwise render as fully qualified identifiers
- render `data_workflow_tools.goose.goose-glm-5-3` as `GLM-5.3`
- render `goose-claude-4-6-sonnet`, `goose-claude-4-7-opus`, and
`goose-kimi-2-7` as `Claude Sonnet 4.6`, `Claude Opus 4.7`, and `Kimi
2.7`
- make the Global Defaults closed model picker use the provider-scoped
display label while preserving the raw discovered model ID as the
persisted value
- remove the obsolete `keepSelectedModelValueLabel` escape hatch and its
raw-label override path so selected discovered models have one
consistent display behavior
- classify the exact discovered Goose Claude IDs with their canonical
adaptive-thinking capability axes, including Sonnet 4.6's exclusion of
`xhigh`
- expand Rust and TypeScript alias coverage and regenerate the shared
139-vector capability corpus
## Test plan
- `cargo test -p buzz-agent --lib` — 517 passed, 1 ignored
- `cd desktop && pnpm test` — 5,821 passed
- Desktop TypeScript typecheck — passed
- Biome on the changed component — passed
- `git diff --check` — passed
- targeted Playwright Global Defaults regression — passed on the
preceding implementation head; the subsequent commit only removes dead
picker-prop plumbing
Verified at `b9609d12696173aa309d2dbaf4f093a502756c36`. The hook-bound
push exceeded the harness timeout in unrelated Rust doc tests, so the
already-verified rebased commit was pushed with hooks bypassed.
Follow-up to #6955.
---------
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
* feat(desktop): add isolated named demo builds (#6407)
🤖 I’m Larry, updating this description on Logan’s behalf.
## Summary
Build named macOS demo apps without Finder automation or collisions with
installed Buzz. `just desktop-demo-build "PR 6407 Demo"` produces a
matching app and DMG, with a fresh build identity even when the same
display name is reused.
- The headless DMG packager uses `hdiutil`; optional Finder styling is
bounded. The existing production release recipe is unchanged.
- Each demo has independent app data, keychain, nest, CLI name,
voice-model storage, repository discovery, and agent OAuth/config
storage. Reset preserves production and sibling-demo state, and retains
retry intent when credential removal or root resolution fails.
- Native links accept only the active build’s registered scheme, then
translate validated entity links into the frontend’s canonical `buzz:`
format.
- The recipe builds all six executable sidecars. Display names are
capped at 31 ASCII characters so the generated identity fits Rust’s
build-time limit.
**Open delivery requirement:** downloaded demos must run without a
Gatekeeper security override. The current recipe is ad-hoc signed and
unnotarized; it does **not** satisfy this requirement. Trusted
branch-demo signing/distribution remains blocked on establishing an
approved signing path. This PR is not being presented as complete
download-and-run delivery.
### Related issue
N/A — reported in the Buzz DMG-packaging workstream.
### Testing
At `11ce21ff97cb387ad676e7caa65b00964097d0bb`, macOS Blox passed the
Tauri workspace suite and compiled-flags gate (including the full
named-demo state; each library pass: 2,992 passed, 19 ignored), Tauri
all-target clippy, the full `buzz-agent` package suite, and frontend
lint/typecheck plus 5,733 tests. Regression coverage includes
cold-start/running entity-link handling, wrong-build rejection, OAuth
deletion failure and retry, unresolved credential roots, and
production/sibling preservation.
At the same head, an extra full named-demo/mesh-enabled run had 3,092
passing tests and one failure: a pre-existing shared-compute `auto`
versus `mesh` expectation, also reproduced on the old published head
`a77b25eca`. The ordinary and demo-state matrix above passes; this is
not an all-features-green claim. Live macOS Launch Services delivery
remains unverified.
GitHub CI completed with 30 successful checks and 9 skipped. The
exact-range security review has not run; its authorization notice
remains open. CI success does not establish trusted signing or
downloaded-app launch.
Earlier demo artifacts established matching app/DMG names, side-by-side
launch, and six non-empty executable arm64 sidecars. These screenshots
show an earlier artifact, not a new build of the final repair commit.
Signature-integrity checks are not Gatekeeper/notarization evidence.
<img width="1032" height="548" alt="Buzz PR 6407 Demo disk image
containing the matching app"
src="https://github.com/user-attachments/assets/bca0277e-db03-4308-b280-fcad55e6d601"
/>
<img width="1186" height="821" alt="Buzz PR 6407 Demo running alongside
other Buzz installations"
src="https://github.com/user-attachments/assets/b4bf4ae5-c341-4e15-8090-9d2ea7c623b6"
/>
---------
Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Other Brother Darryl <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: Larry <loganj+sandbox-larry@squareup.com>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
---------
Signed-off-by: Philip Azar <pazar@squareup.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
Signed-off-by: Fizz <fizz@buzz.local>
Signed-off-by: tulsi <tulsi@block.xyz>
Signed-off-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
Signed-off-by: Jude Edwards <judeedwards@squareup.com>
Signed-off-by: Salman Mohammed <smohammed@squareup.com>
Signed-off-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: shiv <shivchander.s30@gmail.com>
Co-authored-by: Phil Azar <pazar@squareup.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Arjun Mahanti <arjun.mahanti@gmail.com>
Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Co-authored-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
Co-authored-by: LioLionel <62820906+LioLionel@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Fizz <fizz@buzz.local>
Co-authored-by: tulsi <tulsi@block.xyz>
Co-authored-by: thomaspblock <thomasp@squareup.com>
Co-authored-by: Wintermute <c0fc581234c3585602139eec347ced7b82af65b6f6c10728348515c0c06c51c3@buzz.block.builderlab.xyz>
Co-authored-by: Jude Edwards <judeedwards@squareup.com>
Co-authored-by: Salman Mohammed <smohammed@squareup.com>
Co-authored-by: Leo <5faf251baee50ee6bcde338aef6acdd70bb3e60115664c2cd490d94a55dfc488@buzz.block.builderlab.xyz>
Co-authored-by: Cea Stapleton Cordasco <261786559+cea@users.noreply.github.com>
Co-authored-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Co-authored-by: Kalvin C <kalvinnchau@users.noreply.github.com>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: Logan Johnson <loganj@squareup.com>
Co-authored-by: Other Brother Darryl <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: Larry <loganj+sandbox-larry@squareup.com>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bound-membership * origin/main: (32 commits) fix(model-capabilities): humanize databricks goose model names (#7135) feat(db): add NIP-FI identity and final-admission schema foundation (#6994) feat(buzz-acp): give each channel thread its own agent session (#6732) docs: add review-proven failure-path & async-state rules to AGENTS.md (#7061) fix(desktop): back split thread headers (#7137) add public descriptions to agent personas (#7126) feat(desktop): add protected-build Bestie experiment (#6902) fix(relay): reject a frame on its own acknowledgement channel (#6961) fix(acp): wake agents from workflow messages (#6953) feat: render agent avatars as squircles (#7106) fix(ci): salvage Codex review output on PTY-shutdown hang (#7042) fix: retrieving cold memories; add regression task (#6950) Enforce NIP-OA authorization time bounds (#7004) feat(db): configurable writer session timeouts (lock, idle-txn, statement) (#6229) feat(desktop): use segmented controls for channel creation (#6845) feat(buzz-agent): surface stop reason and silent-turn WARN in telemetry (#7038) fix(desktop): surface channel history load failures (#7013) fix(composer): polish automatic mentions (#6956) fix(desktop): resolve bundled sidecar on cheap path and bound login-shell spawns (#6904) perf(mobile): reduce cold startup and channel rendering delays (#6996) ... Signed-off-by: Storme Drone <49c46e84758b2ebff4abf5abbbd44ee4ce788fc3b55db9fa703eec124eead621@buzz.block.builderlab.xyz> # Conflicts: # desktop/src/testing/e2eBridge.ts
Category: improvement
User Impact: Automatic mentions are easier to turn off and now behave consistently across conversations, settings, drafts, and repeated agent mentions.
Problem: People found the new automatic mention behavior hard to control: turning it off in Settings did not reliably affect the composer, removing a mention could require also disabling the feature, and root/thread composers could inherit or restore surprising state. Other reported rough edges included only one of several mentioned agents becoming automatic, synthetic mentions leaking into drafts, restored mentions corrupting adjacent text, controls remaining visible in archived channels, and unclear picker feedback. See the original feedback thread.
Solution: Polish the existing feature around the problems people encountered, keeping automatic mentions controllable and scoped to the active conversation.
Changes
File changes
desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs
Adds coverage for the channel-roster eligibility rules used by agent mention autocomplete.
desktop/src/features/agents/lib/agentAutocompleteEligibility.ts
Aligns agent autocomplete eligibility with channel membership so available agents and their labels stay trustworthy.
desktop/src/features/channels/ui/MembersSidebar.tsx
Uses the shared member-pubkey logic when presenting and acting on channel members.
desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs
Covers preference changes that must remain stable while composer controls are toggled.
desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts
Keeps the automatic-mention preference as durable user intent rather than transient composer state.
desktop/src/features/messages/lib/mentionMemberPubkeys.ts
Centralizes which member identities count as mentionable in the current channel.
desktop/src/features/messages/lib/persistentAgentAudience.test.mjs
Expands lifecycle coverage for persistent agent audiences, explicit exclusions, and restored mentions.
desktop/src/features/messages/lib/persistentAgentAudience.ts
Models automatic, explicit, and excluded agent audiences separately so user choices survive updates without leaking across composers.
desktop/src/features/messages/lib/stripImplicitAgentMentions.test.mjs
Verifies implicit automatic mentions are removed without damaging surrounding separators or authored content.
desktop/src/features/messages/lib/stripImplicitAgentMentions.ts
Strips presentation-only automatic mentions before draft persistence while preserving whitespace and authored text.
desktop/src/features/messages/lib/useMentions.ts
Routes mention insertion and removal through the composer-local audience lifecycle.
desktop/src/features/messages/lib/useRichTextEditor.ts
Preserves mention-chip structure and caret placement when automatic mentions are restored.
desktop/src/features/messages/ui/ComposerAddressControls.test.mjs
Updates control-state expectations for disabled automatic mentions and restored pin affordances.
desktop/src/features/messages/ui/ComposerAddressControls.tsx
Makes automatic-mention state, disabled presentation, and pin controls visually explicit.
desktop/src/features/messages/ui/MentionAutocomplete.test.mjs
Adds coverage for roster labels, pin state, and picker behavior after mention selection.
desktop/src/features/messages/ui/MentionAutocomplete.tsx
Keeps the shortcut picker open for repeated selection and restores visible automatic-mention pin indicators.
desktop/src/features/messages/ui/MessageComposer.tsx
Scopes automatic mention state to each root or thread composer and coordinates restoration, draft persistence, and sending.
desktop/src/features/messages/ui/MessageComposerToolbar.tsx
Passes the effective automatic-mention state into the toolbar presentation.
desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs
Updates keyboard interaction coverage for toggling agents in place.
desktop/src/features/messages/ui/useAddressedAgentMentionRestore.ts
Restores automatic mention chips after lifecycle changes without moving or duplicating authored content.
desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs
Substantially expands coverage for toggles, exclusions, synchronization, and picker dismissal rules.
desktop/src/features/messages/ui/useAgentAddressLockPicker.ts
Keeps the picker usable across repeated choices and preserves explicit per-agent intent while settings change.
desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts
Makes the keyboard shortcut toggle the highlighted automatic audience choice without replacing unrelated selections.
desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts
Owns composer-local automatic-mention lifecycle behavior, including restoration, exclusions, deletion, and disabled-state handling.
desktop/src/features/messages/ui/useComposerMentionPicker.test.mjs
Adds focused picker lifecycle coverage for selection, hover, and dismissal behavior.
desktop/src/features/messages/ui/useComposerMentionPicker.ts
Prevents premature picker dismissal while the user is interacting with its controls.
desktop/src/features/messages/ui/useDraftPersistSnapshot.ts
Persists only user-authored draft content rather than implicit automatic mention decorations.
desktop/src/shared/lib/keyboard-shortcuts.ts
Updates the automatic-mention shortcut description to match its toggle behavior.
desktop/src/testing/e2eBridge.ts
Extends the desktop test bridge with the state needed to exercise roster and automatic-mention transitions.
desktop/tests/e2e/mentions.spec.ts
Covers roster-based labels, managed-agent invitation, revocation, and recovery behavior in the complete mention flow.
desktop/tests/e2e/persistent-agent-audience.spec.ts
Adds end-to-end coverage for root/thread isolation, preference synchronization, manual exclusions, draft hygiene, restored chips, separators, hover behavior, and disabled presentation.
Reproduction Steps
Validation
Validated at
34d208b47d64a9816f88e10a46bcfd479e917d75after rebasing ontoorigin/main(69096c9a8):pnpm check: exited successfully; 4 warnings and 5 informational findings are in unrelated files introduced by current main.Screenshots/Demos
The behavioral changes are covered by the focused desktop E2E scenarios above. Screenshots can be attached from the screenshot-producing automatic-mention E2E after the PR is created.