Skip to content

fix(desktop): retain distinct agent instances in autocomplete - #5202

Merged
atishpatel merged 2 commits into
mainfrom
workerbee/fix-agent-mention-duplicates
Aug 7, 2026
Merged

fix(desktop): retain distinct agent instances in autocomplete#5202
atishpatel merged 2 commits into
mainfrom
workerbee/fix-agent-mention-duplicates

Conversation

@atishpatel

Copy link
Copy Markdown
Contributor

Summary

  • preserve each distinct agent pubkey in autocomplete even when agents share a persona or owner/name
  • continue to collapse duplicate source rows for the same normalized pubkey
  • show a truncated pubkey in the channel member-add picker so same-named instances are selectable

Validation

  • pnpm --filter buzz test — 4,489 passed
  • pnpm --filter buzz exec tsc --noEmit --pretty false
  • pnpm --filter buzz exec biome check src/features/agents/lib/agentAutocompleteEligibility.ts src/features/agents/lib/agentAutocompleteEligibility.test.mjs src/features/channels/ui/MembersSidebar.tsx
  • independent validation by Fast Fizz on 509cb8d97b82f9708e24d4d59ad17c7b39516643: typecheck, focused Biome, 22/22 focused tests, and git diff --check

Generated by Hardworking Honey.

Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
@wesbillman

Copy link
Copy Markdown
Collaborator

Thorough review by Carl on Wes’s behalf at exact head 509cb8d97b82f9708e24d4d59ad17c7b39516643: this fix is needed, correctly scoped, and not covered by PR #4999. No code blocker found.

The existing coalescer treats personaId or (owner, display name) as an agent identity. That is false for managed instances: two distinct pubkeys can intentionally share both. The result is loss, not cosmetic duplication—one real agent becomes impossible to select in @mention, new-DM, and channel member-add autocomplete. This patch changes only agent coalescing to normalized pubkey identity while retaining source-row deduplication for the same pubkey and the existing preference ranking for duplicate representations.

#4999 is complementary, not duplicative. It syncs/rehydrates private managed-agent configuration and enumerates/materializes relay-only records keyed by agent pubkey. It does not modify agentAutocompleteEligibility.ts, MembersSidebar.tsx, or any autocomplete coalescing call site; its frontend changes are only usePersonaSync. Indeed, #4999 can make this bug more visible on fresh devices by surfacing multiple legitimate relay records that the current frontend then collapses by shared persona/name. #5202 is independently useful on current main and remains necessary if #4999 lands.

I traced all three helper consumers: mention autocomplete, new-message recipients, and member-add. Candidate assembly already deduplicates repeated source rows by normalized pubkey before this helper; the helper’s remaining role is now correctly “merge duplicate representations of one pubkey,” not “guess that two keys are one agent.” Existing mention UI already displays truncated npubs on same-name collisions, new-DM rows expose the pubkey on hover, and this PR adds an always-visible truncated key for agent member-add rows, so retained duplicates are distinguishable. Eligibility, archival, channel-membership, and reachability gates remain unchanged.

Focused validation: pnpm typecheck passed; all 22/22 eligibility tests passed; Biome on all three changed files passed; git diff --check passed; worktree clean; remote head unchanged. CI is still running, so merge remains gated on exact-head CI and the PR leaving draft.

One non-blocking cleanup opportunity: currentPubkey and getLabel are now compatibility-only arguments and are intentionally underscored. Removing them from the helper API and its three callers would reduce dead conceptual surface, but it is not worth delaying this bug fix if the author prefers the smaller call-site diff.

@atishpatel
atishpatel marked this pull request as ready for review August 7, 2026 16:50
@atishpatel
atishpatel requested a review from a team as a code owner August 7, 2026 16:50
Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
@atishpatel
atishpatel enabled auto-merge (squash) August 7, 2026 17:18
@atishpatel
atishpatel merged commit e9925db into main Aug 7, 2026
26 checks passed
@atishpatel
atishpatel deleted the workerbee/fix-agent-mention-duplicates branch August 7, 2026 17:32
atishpatel added a commit that referenced this pull request Aug 7, 2026
…-log-harness

* origin/main:
  feat(desktop): adding rich link previews to messages (#3818)
  fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
  fix(desktop): retain distinct agent instances in autocomplete (#5202)
  fix(desktop): defer channel visibility change to Save (#5203)
  feat(desktop): Projects follow-ups — access restrictions, fast loading, activity feed polish (#5073)
  refactor(cli): replace probe/decider/detail split with single typed extractor (#5191)
  fix(desktop): drop unhandled rejection from throwing window.Notification (#5143)
  fix(desktop): fence localStorage SecurityError from killing the React tree (#5142)
  fix(desktop): make terminal output selectable (#4980)
  fix(desktop): use WEBKIT_DMABUF_RENDERER_FORCE_SHM for NVIDIA/AppImage (#3654) (#4505)
  Make public starter channels best effort (#5192)
  Mobile: add anchored reaction popover (#5025)
  feat(mobile): add bee pull-to-refresh (#5059)

Signed-off-by: Atish Patel <atish@squareup.com>
kbst9 added a commit to kbst9/buzz that referenced this pull request Aug 7, 2026
Conflict resolutions, upstream-as-default:
- config.rs: drop the fork's require_media_get_auth staging flag — upstream
  block#4610 made authenticated media reads unconditional; keep file_index_enabled.
- ingest.rs: kind-list unions (upstream KIND_PRIVATE_MANAGED_AGENT + fork
  KIND_SWARM/KIND_COMMUNITY_GUIDE).
- Link previews (block#3818) x swarm tags: splitOutgoingTags routes both tag
  kinds; sendChannelMessage carries linkPreviewTags (upstream position) and
  swarmTags (appended); HomeView caller realigned — positional same-typed
  params, invisible to tsc.
- ChannelPane: upstream WelcomeComposerGuidanceLayer render kept, driven by
  the fork's useWelcomeComposerBanner state; old Banner import dropped.
- swarmDialogState: adopt block#5202 pubkey-keyed agent identity — distinct
  same-name agents stay separate swarm picker options (collapse made one
  unpickable); fork test updated.

SettingsPanels.tsx grew 930->1010 from upstream's side — over-cap, split
owed (same bucket as MembersSidebar). Verified: tsc, desktop suite (4663),
cargo check buzz-relay, fmt.

Signed-off-by: kbst9 <kevinbsteiner@gmail.com>
wpfleger96 pushed a commit that referenced this pull request Aug 7, 2026
* origin/main: (32 commits)
  Recover from max-token response truncation (#5223)
  chore(release): release Buzz Desktop version 0.5.6 (#5214)
  fix(mobile): keep latest messages above composer (#4981)
  fix(sdk): preserve self-mention p tags in message and forum event builders (#4975)
  bump @tauri-apps/cli to ~2.11.4 to fix linux app icon issue (#4858)
  feat(desktop): adding rich link previews to messages (#3818)
  fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
  fix(desktop): retain distinct agent instances in autocomplete (#5202)
  fix(desktop): defer channel visibility change to Save (#5203)
  feat(desktop): Projects follow-ups — access restrictions, fast loading, activity feed polish (#5073)
  refactor(cli): replace probe/decider/detail split with single typed extractor (#5191)
  fix(desktop): drop unhandled rejection from throwing window.Notification (#5143)
  fix(desktop): fence localStorage SecurityError from killing the React tree (#5142)
  fix(desktop): make terminal output selectable (#4980)
  fix(desktop): use WEBKIT_DMABUF_RENDERER_FORCE_SHM for NVIDIA/AppImage (#3654) (#4505)
  Make public starter channels best effort (#5192)
  Mobile: add anchored reaction popover (#5025)
  feat(mobile): add bee pull-to-refresh (#5059)
  Remove agent creation success modal (#5063)
  fix(buzz-agent): escalate LLM timeouts per retry and log per-call latency (#5130)
  ...

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

# Conflicts:
#	desktop/src/shared/api/tauri.ts
wpfleger96 pushed a commit that referenced this pull request Aug 7, 2026
…format

* origin/main: (60 commits)
  feat(desktop): unify add agent flows (#5015)
  fix(buzz-agent): budget summarizer reasoning separately so it cannot starve the handoff summary (#5248)
  infra: bind development services to loopback (#4871)
  chore(release): release Buzz Desktop version 0.5.7 (#5252)
  fix(desktop): isolate relay admission tests (#5221)
  fix(desktop): externalize boot <style> to prevent Tauri CSP nonce override (#5242)
  fix(desktop): let imported and recovered identities finish onboarding (#5228)
  Recover from max-token response truncation (#5223)
  chore(release): release Buzz Desktop version 0.5.6 (#5214)
  fix(mobile): keep latest messages above composer (#4981)
  fix(sdk): preserve self-mention p tags in message and forum event builders (#4975)
  bump @tauri-apps/cli to ~2.11.4 to fix linux app icon issue (#4858)
  feat(desktop): adding rich link previews to messages (#3818)
  fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
  fix(desktop): retain distinct agent instances in autocomplete (#5202)
  fix(desktop): defer channel visibility change to Save (#5203)
  feat(desktop): Projects follow-ups — access restrictions, fast loading, activity feed polish (#5073)
  refactor(cli): replace probe/decider/detail split with single typed extractor (#5191)
  fix(desktop): drop unhandled rejection from throwing window.Notification (#5143)
  fix(desktop): fence localStorage SecurityError from killing the React tree (#5142)
  ...

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
41fred pushed a commit to 41fred/buzz that referenced this pull request Aug 9, 2026
…5202)

## Summary
- preserve each distinct agent pubkey in autocomplete even when agents
share a persona or owner/name
- continue to collapse duplicate source rows for the same normalized
pubkey
- show a truncated pubkey in the channel member-add picker so same-named
instances are selectable

## Validation
- `pnpm --filter buzz test` — 4,489 passed
- `pnpm --filter buzz exec tsc --noEmit --pretty false`
- `pnpm --filter buzz exec biome check
src/features/agents/lib/agentAutocompleteEligibility.ts
src/features/agents/lib/agentAutocompleteEligibility.test.mjs
src/features/channels/ui/MembersSidebar.tsx`
- independent validation by Fast Fizz on
`509cb8d97b82f9708e24d4d59ad17c7b39516643`: typecheck, focused Biome,
22/22 focused tests, and `git diff --check`

Generated by Hardworking Honey.

---------

Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
wpfleger96 pushed a commit that referenced this pull request Aug 10, 2026
…gaps

* origin/main: (26 commits)
  fix(sdk): preserve self-mention p tags in message and forum event builders (#4975)
  bump @tauri-apps/cli to ~2.11.4 to fix linux app icon issue (#4858)
  feat(desktop): adding rich link previews to messages (#3818)
  fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
  fix(desktop): retain distinct agent instances in autocomplete (#5202)
  fix(desktop): defer channel visibility change to Save (#5203)
  feat(desktop): Projects follow-ups — access restrictions, fast loading, activity feed polish (#5073)
  refactor(cli): replace probe/decider/detail split with single typed extractor (#5191)
  fix(desktop): drop unhandled rejection from throwing window.Notification (#5143)
  fix(desktop): fence localStorage SecurityError from killing the React tree (#5142)
  fix(desktop): make terminal output selectable (#4980)
  fix(desktop): use WEBKIT_DMABUF_RENDERER_FORCE_SHM for NVIDIA/AppImage (#3654) (#4505)
  Make public starter channels best effort (#5192)
  Mobile: add anchored reaction popover (#5025)
  feat(mobile): add bee pull-to-refresh (#5059)
  Remove agent creation success modal (#5063)
  fix(buzz-agent): escalate LLM timeouts per retry and log per-call latency (#5130)
  fix(agent): resolve oauth cache home cross-platform (#5151)
  Improve video review readiness and controls (#5161)
  Polish advanced agent setup and Welcome composer (#4926)
  ...

Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 16, 2026
Two surfaces answered "which agents exist" with two hand-rolled identity
keys, and they had drifted. @-mention autocomplete keys agents by pubkey
(block#5202). The Agents library grouped by `personaId` and then rendered ONE
card per group via `pickProfileAgent`, so every instance past the first
had no card at all.

That is invisible while a persona's instances all share a name. It stops
being invisible the moment an agent is renamed but keeps its builtin
persona id — the owner's `managed-agents.json` has `builtin:fizz` holding
two "Claude" and two "Fizz" instances, and `builtin:honey` holding two
"Cody" and two "Honey". The library rendered exactly one card for each of
those personas, labelled with the persona name and wired to whichever
instance `pickProfileAgent` returned. Two of eleven agents were invisible
and unmanageable.

One identity definition, shared
-------------------------------
`agents/lib/agentIdentity.ts` now owns the doctrine that used to live as a
comment inside `agentAutocompleteEligibility.ts`:

  agentIdentityKey()      pubkey — THE identity, used by autocomplete
                          coalescing and by the library
  agentDisplayGroupKey()  persona + folded name — presentation only:
                          which agents may share ONE card. Never a
                          substitute for identity; a display group keeps
                          every member identity and callers must keep
                          them all reachable.

Autocomplete now imports `agentIdentityKey` instead of re-deriving it, so
the two surfaces cannot answer this question differently again.

Why not one card per instance
-----------------------------
Exploding to a card per pubkey would have produced 18 agent cards from 11
agents, and it would have reverted a deliberate product decision: same-
named instances of one persona already collapse onto the persona's card
and stay reachable through that card's profile panel (pinned by the e2e
"duplicate instances move from the agents gallery into the agent profile").
The autocomplete argument for never collapsing does not transfer — there,
collapsing makes a pubkey unmentionable; here, the card lists every
instance behind it.

So the collapse stays, bounded by one rule: a card may only stand for
instances whose label it truthfully shows. All-same-name persona group →
one card, labelled with the persona name, exactly as before. Once the
owner has renamed an instance, the persona name can no longer stand for
all of them, so each surviving name gets its own card. The owner's data
goes from 9 cards hiding 9 agents to 11 cards hiding none.

Opening a split card must open that instance, so
`pickCanonicalProfileAgent` canonicalises within the requested instance's
display group rather than across the whole persona. Same-named instances
still collapse onto one profile target; a renamed one opens itself
instead of silently redirecting to its persona sibling.

Tests
-----
`unifiedAgentGroups.ts` had no test file. It has one now, plus
`agentIdentity.test.mjs`, covering: a renamed instance gets a card; no
identity is dropped by persona grouping; the library and autocomplete
agree on the identity set; same-name instances still share one card;
persona actions stay on exactly one card per persona.

No agent records are merged, renamed, or deleted — deletes propagate
cross-device as kind:5 tombstones while the nsec does not, so a merge
would strand agents on other machines.

Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 16, 2026
…listing them

Review follow-up on PR block#6077, and the reviewer is right.

The guard's allowlist was keyed on `path:matchedLiteral` where the literal was
only the namespace prefix, so one entry exempted EVERY occurrence of that
prefix in the file. The four exempted files were the agent-adjacent ones, so
the guard was theatre over precisely the code most likely to drift.

Reproduced before fixing: adding a second, unrelated
`persona:${a.personaId}|${a.name}` to an allowlisted file passed at exit 0.

The fix is not a tighter allowlist but removing the need for one. The two
legitimate non-identity uses get namespaces of their own:

- `catalog-persona:` for the persona catalog dialog's radio-group selection
  token, now a named constant. It addresses a row in that dialog's own list,
  is produced and consumed only in that file, and is never persisted.
- `profile:` for the profile panel's render key, which exists precisely when
  there is NO agent to identify (an uninstantiated persona has no pubkey), so
  looking like an agent identity was the wrong signal to send.

The allowlist is now empty, and documented as worth keeping that way.

The match also widened from the bare prefix to the prefix plus the first
interpolation or word after it, so if an entry ever does become unavoidable it
scopes to one literal rather than the whole file. Both attack cases verified
after the change: the second key in a formerly-exempt file is now caught, and
reintroducing the historical pre-block#5202 `persona:${candidate.personaId}` into
agentAutocompleteEligibility.ts still fails with the file and line named.

One thing worth noting for reviewers: `check-file-sizes` caught a 2-line
comment I had added to UserProfilePanel.tsx, which sits exactly on the
1000-line ceiling (1000 -> 1002). The comment was redundant with the doc on
`profilePanelTargetKey` and is gone; the file is unchanged in length.

Verified: pnpm check exits 0 with all four guards; desktop unit suite
4978/4978; tsc --noEmit clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 16, 2026
Two surfaces answered "which agents exist" with two hand-rolled identity
keys, and they had drifted. @-mention autocomplete keys agents by pubkey
(block#5202). The Agents library grouped by `personaId` and then rendered ONE
card per group via `pickProfileAgent`, so every instance past the first
had no card at all.

That is invisible while a persona's instances all share a name. It stops
being invisible the moment an agent is renamed but keeps its builtin
persona id — the owner's `managed-agents.json` has `builtin:fizz` holding
two "Claude" and two "Fizz" instances, and `builtin:honey` holding two
"Cody" and two "Honey". The library rendered exactly one card for each of
those personas, labelled with the persona name and wired to whichever
instance `pickProfileAgent` returned. Two of eleven agents were invisible
and unmanageable.

One identity definition, shared
-------------------------------
`agents/lib/agentIdentity.ts` now owns the doctrine that used to live as a
comment inside `agentAutocompleteEligibility.ts`:

  agentIdentityKey()      pubkey — THE identity, used by autocomplete
                          coalescing and by the library
  agentDisplayGroupKey()  persona + folded name — presentation only:
                          which agents may share ONE card. Never a
                          substitute for identity; a display group keeps
                          every member identity and callers must keep
                          them all reachable.

Autocomplete now imports `agentIdentityKey` instead of re-deriving it, so
the two surfaces cannot answer this question differently again.

Why not one card per instance
-----------------------------
Exploding to a card per pubkey would have produced 18 agent cards from 11
agents, and it would have reverted a deliberate product decision: same-
named instances of one persona already collapse onto the persona's card
and stay reachable through that card's profile panel (pinned by the e2e
"duplicate instances move from the agents gallery into the agent profile").
The autocomplete argument for never collapsing does not transfer — there,
collapsing makes a pubkey unmentionable; here, the card lists every
instance behind it.

So the collapse stays, bounded by one rule: a card may only stand for
instances whose label it truthfully shows. All-same-name persona group →
one card, labelled with the persona name, exactly as before. Once the
owner has renamed an instance, the persona name can no longer stand for
all of them, so each surviving name gets its own card. The owner's data
goes from 9 cards hiding 9 agents to 11 cards hiding none.

Opening a split card must open that instance, so
`pickCanonicalProfileAgent` canonicalises within the requested instance's
display group rather than across the whole persona. Same-named instances
still collapse onto one profile target; a renamed one opens itself
instead of silently redirecting to its persona sibling.

Tests
-----
`unifiedAgentGroups.ts` had no test file. It has one now, plus
`agentIdentity.test.mjs`, covering: a renamed instance gets a card; no
identity is dropped by persona grouping; the library and autocomplete
agree on the identity set; same-name instances still share one card;
persona actions stay on exactly one card per persona.

No agent records are merged, renamed, or deleted — deletes propagate
cross-device as kind:5 tombstones while the nsec does not, so a merge
would strand agents on other machines.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 16, 2026
The bug the parent commits fix was a drift, not a typo: @-mention autocomplete
and the Agents library each hand-rolled a key for "same agent?", block#5202 moved
one of them to the pubkey, and nobody moved the other. Renaming an instance
then made it disappear from the library entirely. Nothing failed loudly -- a
card simply stopped existing.

Convention alone does not stop that recurring, so this encodes the invariant:
an agent identity or display-group key is minted in exactly one module,
src/features/agents/lib/agentIdentity.ts, and everywhere else imports it. The
guard flags a string or template literal that begins an identity namespace --
`pubkey:` or `persona:` -- anywhere outside that module. Those are the wire
formats agentIdentityKey and agentDisplayGroupKey produce, so a literal
starting with one is either a second implementation or one edit away from
becoming one.

Verified it catches the real thing, not just a synthetic case: restoring the
pre-block#5202 `persona:${candidate.personaId}` into agentAutocompleteEligibility.ts
fails the check and names that file and line. Restoring the fix passes.

Deliberately narrow, because a guard that cries wolf gets disabled:

- It does not try to catch every way of grouping agents, only the shape that
  caused the outage.
- Comment lines are skipped. Unrelated subsystems document their own scope keys
  (channel storage uses "pubkey:normalizedRelayUrl"), and flagging prose
  teaches people to silence the guard rather than read it.
- Four genuine non-identity uses are allowlisted by `path:literal` with a
  reason each: the persona catalog dialog's selection token, and the profile
  panel's render key for a persona that has no agent instance yet. Both are
  adjacent to the real thing, so each entry says what would make it a
  violation.

Wired into `pnpm check` alongside check-file-sizes, check-px-text and
check-pubkey-truncation, matching their structure (single-purpose script,
`path:matchedLiteral` allowlist, failure message that names the fix).

Verified: pnpm check exits 0 with the new guard in the chain; biome clean;
package.json parses.

Stacks on the agent-identity fix -- the invariant has no canonical module to
point at without it.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 16, 2026
…listing them


Review follow-up on PR block#6077, and the reviewer is right.

The guard's allowlist was keyed on `path:matchedLiteral` where the literal was
only the namespace prefix, so one entry exempted EVERY occurrence of that
prefix in the file. The four exempted files were the agent-adjacent ones, so
the guard was theatre over precisely the code most likely to drift.

Reproduced before fixing: adding a second, unrelated
`persona:${a.personaId}|${a.name}` to an allowlisted file passed at exit 0.

The fix is not a tighter allowlist but removing the need for one. The two
legitimate non-identity uses get namespaces of their own:

- `catalog-persona:` for the persona catalog dialog's radio-group selection
  token, now a named constant. It addresses a row in that dialog's own list,
  is produced and consumed only in that file, and is never persisted.
- `profile:` for the profile panel's render key, which exists precisely when
  there is NO agent to identify (an uninstantiated persona has no pubkey), so
  looking like an agent identity was the wrong signal to send.

The allowlist is now empty, and documented as worth keeping that way.

The match also widened from the bare prefix to the prefix plus the first
interpolation or word after it, so if an entry ever does become unavoidable it
scopes to one literal rather than the whole file. Both attack cases verified
after the change: the second key in a formerly-exempt file is now caught, and
reintroducing the historical pre-block#5202 `persona:${candidate.personaId}` into
agentAutocompleteEligibility.ts still fails with the file and line named.

One thing worth noting for reviewers: `check-file-sizes` caught a 2-line
comment I had added to UserProfilePanel.tsx, which sits exactly on the
1000-line ceiling (1000 -> 1002). The comment was redundant with the doc on
`profilePanelTargetKey` and is gone; the file is unchanged in length.

Verified: pnpm check exits 0 with all four guards; desktop unit suite
4978/4978; tsc --noEmit clean.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 17, 2026
The bug the parent commits fix was a drift, not a typo: @-mention autocomplete
and the Agents library each hand-rolled a key for "same agent?", block#5202 moved
one of them to the pubkey, and nobody moved the other. Renaming an instance
then made it disappear from the library entirely. Nothing failed loudly -- a
card simply stopped existing.

Convention alone does not stop that recurring, so this encodes the invariant:
an agent identity or display-group key is minted in exactly one module,
src/features/agents/lib/agentIdentity.ts, and everywhere else imports it. The
guard flags a string or template literal that begins an identity namespace --
`pubkey:` or `persona:` -- anywhere outside that module. Those are the wire
formats agentIdentityKey and agentDisplayGroupKey produce, so a literal
starting with one is either a second implementation or one edit away from
becoming one.

Verified it catches the real thing, not just a synthetic case: restoring the
pre-block#5202 `persona:${candidate.personaId}` into agentAutocompleteEligibility.ts
fails the check and names that file and line. Restoring the fix passes.

Deliberately narrow, because a guard that cries wolf gets disabled:

- It does not try to catch every way of grouping agents, only the shape that
  caused the outage.
- Comment lines are skipped. Unrelated subsystems document their own scope keys
  (channel storage uses "pubkey:normalizedRelayUrl"), and flagging prose
  teaches people to silence the guard rather than read it.
- Four genuine non-identity uses are allowlisted by `path:literal` with a
  reason each: the persona catalog dialog's selection token, and the profile
  panel's render key for a persona that has no agent instance yet. Both are
  adjacent to the real thing, so each entry says what would make it a
  violation.

Wired into `pnpm check` alongside check-file-sizes, check-px-text and
check-pubkey-truncation, matching their structure (single-purpose script,
`path:matchedLiteral` allowlist, failure message that names the fix).

Verified: pnpm check exits 0 with the new guard in the chain; biome clean;
package.json parses.

Stacks on the agent-identity fix -- the invariant has no canonical module to
point at without it.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 17, 2026
…listing them


Review follow-up on PR block#6077, and the reviewer is right.

The guard's allowlist was keyed on `path:matchedLiteral` where the literal was
only the namespace prefix, so one entry exempted EVERY occurrence of that
prefix in the file. The four exempted files were the agent-adjacent ones, so
the guard was theatre over precisely the code most likely to drift.

Reproduced before fixing: adding a second, unrelated
`persona:${a.personaId}|${a.name}` to an allowlisted file passed at exit 0.

The fix is not a tighter allowlist but removing the need for one. The two
legitimate non-identity uses get namespaces of their own:

- `catalog-persona:` for the persona catalog dialog's radio-group selection
  token, now a named constant. It addresses a row in that dialog's own list,
  is produced and consumed only in that file, and is never persisted.
- `profile:` for the profile panel's render key, which exists precisely when
  there is NO agent to identify (an uninstantiated persona has no pubkey), so
  looking like an agent identity was the wrong signal to send.

The allowlist is now empty, and documented as worth keeping that way.

The match also widened from the bare prefix to the prefix plus the first
interpolation or word after it, so if an entry ever does become unavoidable it
scopes to one literal rather than the whole file. Both attack cases verified
after the change: the second key in a formerly-exempt file is now caught, and
reintroducing the historical pre-block#5202 `persona:${candidate.personaId}` into
agentAutocompleteEligibility.ts still fails with the file and line named.

One thing worth noting for reviewers: `check-file-sizes` caught a 2-line
comment I had added to UserProfilePanel.tsx, which sits exactly on the
1000-line ceiling (1000 -> 1002). The comment was redundant with the doc on
`profilePanelTargetKey` and is gone; the file is unchanged in length.

Verified: pnpm check exits 0 with all four guards; desktop unit suite
4978/4978; tsc --noEmit clean.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 17, 2026
Two surfaces answered "which agents exist" with two hand-rolled identity
keys, and they had drifted. @-mention autocomplete keys agents by pubkey
(block#5202). The Agents library grouped by `personaId` and then rendered ONE
card per group via `pickProfileAgent`, so every instance past the first
had no card at all.

That is invisible while a persona's instances all share a name. It stops
being invisible the moment an agent is renamed but keeps its builtin
persona id — the owner's `managed-agents.json` has `builtin:fizz` holding
two "Claude" and two "Fizz" instances, and `builtin:honey` holding two
"Cody" and two "Honey". The library rendered exactly one card for each of
those personas, labelled with the persona name and wired to whichever
instance `pickProfileAgent` returned. Two of eleven agents were invisible
and unmanageable.

One identity definition, shared
-------------------------------
`agents/lib/agentIdentity.ts` now owns the doctrine that used to live as a
comment inside `agentAutocompleteEligibility.ts`:

  agentIdentityKey()      pubkey — THE identity, used by autocomplete
                          coalescing and by the library
  agentDisplayGroupKey()  persona + folded name — presentation only:
                          which agents may share ONE card. Never a
                          substitute for identity; a display group keeps
                          every member identity and callers must keep
                          them all reachable.

Autocomplete now imports `agentIdentityKey` instead of re-deriving it, so
the two surfaces cannot answer this question differently again.

Why not one card per instance
-----------------------------
Exploding to a card per pubkey would have produced 18 agent cards from 11
agents, and it would have reverted a deliberate product decision: same-
named instances of one persona already collapse onto the persona's card
and stay reachable through that card's profile panel (pinned by the e2e
"duplicate instances move from the agents gallery into the agent profile").
The autocomplete argument for never collapsing does not transfer — there,
collapsing makes a pubkey unmentionable; here, the card lists every
instance behind it.

So the collapse stays, bounded by one rule: a card may only stand for
instances whose label it truthfully shows. All-same-name persona group →
one card, labelled with the persona name, exactly as before. Once the
owner has renamed an instance, the persona name can no longer stand for
all of them, so each surviving name gets its own card. The owner's data
goes from 9 cards hiding 9 agents to 11 cards hiding none.

Opening a split card must open that instance, so
`pickCanonicalProfileAgent` canonicalises within the requested instance's
display group rather than across the whole persona. Same-named instances
still collapse onto one profile target; a renamed one opens itself
instead of silently redirecting to its persona sibling.

Tests
-----
`unifiedAgentGroups.ts` had no test file. It has one now, plus
`agentIdentity.test.mjs`, covering: a renamed instance gets a card; no
identity is dropped by persona grouping; the library and autocomplete
agree on the identity set; same-name instances still share one card;
persona actions stay on exactly one card per persona.

No agent records are merged, renamed, or deleted — deletes propagate
cross-device as kind:5 tombstones while the nsec does not, so a merge
would strand agents on other machines.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 17, 2026
The bug the parent commits fix was a drift, not a typo: @-mention autocomplete
and the Agents library each hand-rolled a key for "same agent?", block#5202 moved
one of them to the pubkey, and nobody moved the other. Renaming an instance
then made it disappear from the library entirely. Nothing failed loudly -- a
card simply stopped existing.

Convention alone does not stop that recurring, so this encodes the invariant:
an agent identity or display-group key is minted in exactly one module,
src/features/agents/lib/agentIdentity.ts, and everywhere else imports it. The
guard flags a string or template literal that begins an identity namespace --
`pubkey:` or `persona:` -- anywhere outside that module. Those are the wire
formats agentIdentityKey and agentDisplayGroupKey produce, so a literal
starting with one is either a second implementation or one edit away from
becoming one.

Verified it catches the real thing, not just a synthetic case: restoring the
pre-block#5202 `persona:${candidate.personaId}` into agentAutocompleteEligibility.ts
fails the check and names that file and line. Restoring the fix passes.

Deliberately narrow, because a guard that cries wolf gets disabled:

- It does not try to catch every way of grouping agents, only the shape that
  caused the outage.
- Comment lines are skipped. Unrelated subsystems document their own scope keys
  (channel storage uses "pubkey:normalizedRelayUrl"), and flagging prose
  teaches people to silence the guard rather than read it.
- Four genuine non-identity uses are allowlisted by `path:literal` with a
  reason each: the persona catalog dialog's selection token, and the profile
  panel's render key for a persona that has no agent instance yet. Both are
  adjacent to the real thing, so each entry says what would make it a
  violation.

Wired into `pnpm check` alongside check-file-sizes, check-px-text and
check-pubkey-truncation, matching their structure (single-purpose script,
`path:matchedLiteral` allowlist, failure message that names the fix).

Verified: pnpm check exits 0 with the new guard in the chain; biome clean;
package.json parses.

Stacks on the agent-identity fix -- the invariant has no canonical module to
point at without it.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 17, 2026
…listing them


Review follow-up on PR block#6077, and the reviewer is right.

The guard's allowlist was keyed on `path:matchedLiteral` where the literal was
only the namespace prefix, so one entry exempted EVERY occurrence of that
prefix in the file. The four exempted files were the agent-adjacent ones, so
the guard was theatre over precisely the code most likely to drift.

Reproduced before fixing: adding a second, unrelated
`persona:${a.personaId}|${a.name}` to an allowlisted file passed at exit 0.

The fix is not a tighter allowlist but removing the need for one. The two
legitimate non-identity uses get namespaces of their own:

- `catalog-persona:` for the persona catalog dialog's radio-group selection
  token, now a named constant. It addresses a row in that dialog's own list,
  is produced and consumed only in that file, and is never persisted.
- `profile:` for the profile panel's render key, which exists precisely when
  there is NO agent to identify (an uninstantiated persona has no pubkey), so
  looking like an agent identity was the wrong signal to send.

The allowlist is now empty, and documented as worth keeping that way.

The match also widened from the bare prefix to the prefix plus the first
interpolation or word after it, so if an entry ever does become unavoidable it
scopes to one literal rather than the whole file. Both attack cases verified
after the change: the second key in a formerly-exempt file is now caught, and
reintroducing the historical pre-block#5202 `persona:${candidate.personaId}` into
agentAutocompleteEligibility.ts still fails with the file and line named.

One thing worth noting for reviewers: `check-file-sizes` caught a 2-line
comment I had added to UserProfilePanel.tsx, which sits exactly on the
1000-line ceiling (1000 -> 1002). The comment was redundant with the doc on
`profilePanelTargetKey` and is gone; the file is unchanged in length.

Verified: pnpm check exits 0 with all four guards; desktop unit suite
4978/4978; tsc --noEmit clean.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 17, 2026
Two surfaces answered "which agents exist" with two hand-rolled identity
keys, and they had drifted. @-mention autocomplete keys agents by pubkey
(block#5202). The Agents library grouped by `personaId` and then rendered ONE
card per group via `pickProfileAgent`, so every instance past the first
had no card at all.

That is invisible while a persona's instances all share a name. It stops
being invisible the moment an agent is renamed but keeps its builtin
persona id — the owner's `managed-agents.json` has `builtin:fizz` holding
two "Claude" and two "Fizz" instances, and `builtin:honey` holding two
"Cody" and two "Honey". The library rendered exactly one card for each of
those personas, labelled with the persona name and wired to whichever
instance `pickProfileAgent` returned. Two of eleven agents were invisible
and unmanageable.

One identity definition, shared
-------------------------------
`agents/lib/agentIdentity.ts` now owns the doctrine that used to live as a
comment inside `agentAutocompleteEligibility.ts`:

  agentIdentityKey()      pubkey — THE identity, used by autocomplete
                          coalescing and by the library
  agentDisplayGroupKey()  persona + folded name — presentation only:
                          which agents may share ONE card. Never a
                          substitute for identity; a display group keeps
                          every member identity and callers must keep
                          them all reachable.

Autocomplete now imports `agentIdentityKey` instead of re-deriving it, so
the two surfaces cannot answer this question differently again.

Why not one card per instance
-----------------------------
Exploding to a card per pubkey would have produced 18 agent cards from 11
agents, and it would have reverted a deliberate product decision: same-
named instances of one persona already collapse onto the persona's card
and stay reachable through that card's profile panel (pinned by the e2e
"duplicate instances move from the agents gallery into the agent profile").
The autocomplete argument for never collapsing does not transfer — there,
collapsing makes a pubkey unmentionable; here, the card lists every
instance behind it.

So the collapse stays, bounded by one rule: a card may only stand for
instances whose label it truthfully shows. All-same-name persona group →
one card, labelled with the persona name, exactly as before. Once the
owner has renamed an instance, the persona name can no longer stand for
all of them, so each surviving name gets its own card. The owner's data
goes from 9 cards hiding 9 agents to 11 cards hiding none.

Opening a split card must open that instance, so
`pickCanonicalProfileAgent` canonicalises within the requested instance's
display group rather than across the whole persona. Same-named instances
still collapse onto one profile target; a renamed one opens itself
instead of silently redirecting to its persona sibling.

Tests
-----
`unifiedAgentGroups.ts` had no test file. It has one now, plus
`agentIdentity.test.mjs`, covering: a renamed instance gets a card; no
identity is dropped by persona grouping; the library and autocomplete
agree on the identity set; same-name instances still share one card;
persona actions stay on exactly one card per persona.

No agent records are merged, renamed, or deleted — deletes propagate
cross-device as kind:5 tombstones while the nsec does not, so a merge
would strand agents on other machines.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 17, 2026
The bug the parent commits fix was a drift, not a typo: @-mention autocomplete
and the Agents library each hand-rolled a key for "same agent?", block#5202 moved
one of them to the pubkey, and nobody moved the other. Renaming an instance
then made it disappear from the library entirely. Nothing failed loudly -- a
card simply stopped existing.

Convention alone does not stop that recurring, so this encodes the invariant:
an agent identity or display-group key is minted in exactly one module,
src/features/agents/lib/agentIdentity.ts, and everywhere else imports it. The
guard flags a string or template literal that begins an identity namespace --
`pubkey:` or `persona:` -- anywhere outside that module. Those are the wire
formats agentIdentityKey and agentDisplayGroupKey produce, so a literal
starting with one is either a second implementation or one edit away from
becoming one.

Verified it catches the real thing, not just a synthetic case: restoring the
pre-block#5202 `persona:${candidate.personaId}` into agentAutocompleteEligibility.ts
fails the check and names that file and line. Restoring the fix passes.

Deliberately narrow, because a guard that cries wolf gets disabled:

- It does not try to catch every way of grouping agents, only the shape that
  caused the outage.
- Comment lines are skipped. Unrelated subsystems document their own scope keys
  (channel storage uses "pubkey:normalizedRelayUrl"), and flagging prose
  teaches people to silence the guard rather than read it.
- Four genuine non-identity uses are allowlisted by `path:literal` with a
  reason each: the persona catalog dialog's selection token, and the profile
  panel's render key for a persona that has no agent instance yet. Both are
  adjacent to the real thing, so each entry says what would make it a
  violation.

Wired into `pnpm check` alongside check-file-sizes, check-px-text and
check-pubkey-truncation, matching their structure (single-purpose script,
`path:matchedLiteral` allowlist, failure message that names the fix).

Verified: pnpm check exits 0 with the new guard in the chain; biome clean;
package.json parses.

Stacks on the agent-identity fix -- the invariant has no canonical module to
point at without it.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
mfethe1 pushed a commit to mfethe1/buzz that referenced this pull request Aug 17, 2026
…listing them


Review follow-up on PR block#6077, and the reviewer is right.

The guard's allowlist was keyed on `path:matchedLiteral` where the literal was
only the namespace prefix, so one entry exempted EVERY occurrence of that
prefix in the file. The four exempted files were the agent-adjacent ones, so
the guard was theatre over precisely the code most likely to drift.

Reproduced before fixing: adding a second, unrelated
`persona:${a.personaId}|${a.name}` to an allowlisted file passed at exit 0.

The fix is not a tighter allowlist but removing the need for one. The two
legitimate non-identity uses get namespaces of their own:

- `catalog-persona:` for the persona catalog dialog's radio-group selection
  token, now a named constant. It addresses a row in that dialog's own list,
  is produced and consumed only in that file, and is never persisted.
- `profile:` for the profile panel's render key, which exists precisely when
  there is NO agent to identify (an uninstantiated persona has no pubkey), so
  looking like an agent identity was the wrong signal to send.

The allowlist is now empty, and documented as worth keeping that way.

The match also widened from the bare prefix to the prefix plus the first
interpolation or word after it, so if an entry ever does become unavoidable it
scopes to one literal rather than the whole file. Both attack cases verified
after the change: the second key in a formerly-exempt file is now caught, and
reintroducing the historical pre-block#5202 `persona:${candidate.personaId}` into
agentAutocompleteEligibility.ts still fails with the file and line named.

One thing worth noting for reviewers: `check-file-sizes` caught a 2-line
comment I had added to UserProfilePanel.tsx, which sits exactly on the
1000-line ceiling (1000 -> 1002). The comment was redundant with the doc on
`profilePanelTargetKey` and is gone; the file is unchanged in length.

Verified: pnpm check exits 0 with all four guards; desktop unit suite
4978/4978; tsc --noEmit clean.

Co-authored-by: Michael Feth <michael@jira-flow.com>
Signed-off-by: Michael Feth <michael@jira-flow.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants