fix(desktop): retain distinct agent instances in autocomplete - #5202
Conversation
Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
|
Thorough review by Carl on Wes’s behalf at exact head The existing coalescer treats #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 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: One non-blocking cleanup opportunity: |
Co-authored-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz> Signed-off-by: Hardworking Honey <c5c455215c2506cb8ba776518cec804af62d3a0526e32d496a22072e395042b9@buzz.block.builderlab.xyz>
…-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>
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>
* 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
…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>
…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>
…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>
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>
…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>
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>
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>
…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>
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>
…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>
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>
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>
…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>
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>
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>
…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>
Summary
Validation
pnpm --filter buzz test— 4,489 passedpnpm --filter buzz exec tsc --noEmit --pretty falsepnpm --filter buzz exec biome check src/features/agents/lib/agentAutocompleteEligibility.ts src/features/agents/lib/agentAutocompleteEligibility.test.mjs src/features/channels/ui/MembersSidebar.tsx509cb8d97b82f9708e24d4d59ad17c7b39516643: typecheck, focused Biome, 22/22 focused tests, andgit diff --checkGenerated by Hardworking Honey.