Skip to content

fix(desktop): deduplicate Bots roster and reuse canonical chats - #88341

Closed
frizikk wants to merge 1 commit into
NousResearch:mainfrom
frizikk:fix/bots-roster-dedup
Closed

fix(desktop): deduplicate Bots roster and reuse canonical chats#88341
frizikk wants to merge 1 commit into
NousResearch:mainfrom
frizikk:fix/bots-roster-dedup

Conversation

@frizikk

@frizikk frizikk commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the Desktop BOTS pane creating repeated rows for one agent identity and avoids creating an extra canonical session on first open.

The roster is now deduplicated by (connectionId, profile) in both the Electron source and the plugin merger. When a bot has no stored Desktop pin, the canonical opener first adopts an existing Bot Chat created through CLI/A2A instead of immediately minting another session.

Related Issue

Fixes #88340

Related: #88200 (adjacent preview/click routing issue; not closed by this PR).

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Tests
  • Refactoring only

Changes Made

  • Collapse repeated local/remote profile reports into one routable roster identity while retaining same-named profiles from different connections.
  • Add a plugin-boundary deduplication guard for stale shells and reconnect races.
  • Reuse an existing titled Bot Chat before creating a new canonical chat without a saved UI pin.
  • Add Electron roster, multi-source plugin roster, and canonical-chat regression coverage.

How to Test

  1. Run npm --workspace apps/desktop exec vitest run electron/connection-registry.test.ts.
  2. Run npm --workspace apps/desktop run check:test:plugins.
  3. In Desktop BOTS, feed repeated default reports for the same connection and verify a single @default-this-device row remains.
  4. Create a CLI/A2A Bot Chat without Desktop ui_meta.chat, then open that bot from BOTS and verify the existing session is opened rather than a new one created.

Checklist

  • My code follows the project's style guidelines.
  • I have performed a self-review of my code.
  • I have added tests that prove the regression and expected behavior.
  • Type checking passes.
  • I searched existing GitHub issues before opening the focused issue above.
  • I kept the change scoped to BOTS roster identity and canonical-chat reuse.
  • I manually exercised the packaged Desktop UI (automated coverage only).

Verification

  • npm --workspace apps/desktop exec vitest run electron/connection-registry.test.ts — 44 passed.
  • npm --workspace apps/desktop run check:test:plugins — 151 passed.
  • npm --workspace apps/desktop run typecheck — passed.
  • git diff --check — passed.

npm --workspace apps/desktop run test:ui has four unrelated failures on this branch, reproduced unchanged on clean origin/main: locale formatting expectations in use-prompt-actions/utils.test.ts and billing/index.test.tsx, plus a truncation-label expectation in fallback-model.test.ts.
obraz

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 17, 2026
@ayushnangia

Copy link
Copy Markdown
Contributor

Good catch on the UI half — this pairs with #88331, which fixes the other half of #88268 at the DB layer (per-job run-history pruning so per-tick cron rows stop growing state.db and feeding the roster). Different layers, no overlap: yours dedups what's shown, ours bounds what's stored. Reviewers can take them independently.

@ScaleLeanChris

Copy link
Copy Markdown
Contributor

Current-main overlap check with #88328 and #88380:

This PR contains two independently useful pieces, but its plugin-side roster merge now overlaps #88328.

I applied #88328 and #88380 to current main 4323c67dcc; that combined tree passed 154 Bot Mode plugin tests plus 107 Electron/SDK routing tests. Applying this PR afterward conflicts only in plugin.js::mergeMultiSourceRoster and its focused test. The conflict is substantive: #88328 already deduplicates rich rows by profile, union rows by (connectionId, profile), avoids mutating reused gateway rows, and distinguishes the active remote source by connection id.

The non-overlapping parts here still add value:

  • Electron connection-registry.ts deduplication before data crosses the plugin boundary.
  • Existing Bot Chat adoption before creating a new canonical session.

Recommendation: rebase after #88328, drop this PRs competing mergeMultiSourceRoster implementation, retain the Electron dedupe and canonical-chat reuse, and keep only the focused coverage for those retained behaviors. That should preserve the stronger source-provenance model while still fixing the extra-session half of #88340/#88436.

teknium1 added a commit that referenced this pull request Aug 17, 2026
…source-qualified row keys

Layers on the salvaged #88489 (@29206394) and #88341 (@frizikk):

- sdk: host.activeConnectionId() — registry id of the LIVE active gateway.
  The salvaged fix classifies against the registry primary; after the user
  activates a non-primary source's agent, profiles.list answers from THAT
  source and primary-based matching would duplicate the active source's
  agents again. Live id wins, primaryConnectionId is the fallback, the
  legacy kind==='local' rule covers older desktops.
- plugin: roster/chip/picker list keys are botRowKey(bot) — source-qualified
  (connectionId, name) — so same-named agents on two genuine sources can
  never collide as duplicate React keys (the render half of the dupe-bots
  smear: name-keyed rows + duplicate names = repeated blocks every poll).
  Annotated active-source rows keep the plain-name key, so nothing remounts
  when a desktop gains the union roster.
- tests: live-id-beats-primary regression, botRowKey stability, source-shape
  anchor refresh.
teknium1 added a commit that referenced this pull request Aug 17, 2026
…source-qualified row keys

Layers on the salvaged #88489 (@29206394) and #88341 (@frizikk):

- sdk: host.activeConnectionId() — registry id of the LIVE active gateway.
  The salvaged fix classifies against the registry primary; after the user
  activates a non-primary source's agent, profiles.list answers from THAT
  source and primary-based matching would duplicate the active source's
  agents again. Live id wins, primaryConnectionId is the fallback, the
  legacy kind==='local' rule covers older desktops.
- plugin: roster/chip/picker list keys are botRowKey(bot) — source-qualified
  (connectionId, name) — so same-named agents on two genuine sources can
  never collide as duplicate React keys (the render half of the dupe-bots
  smear: name-keyed rows + duplicate names = repeated blocks every poll).
  Annotated active-source rows keep the plain-name key, so nothing remounts
  when a desktop gains the union roster.
- tests: live-id-beats-primary regression, botRowKey stability, source-shape
  anchor refresh.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #88523 — your commit was cherry-picked with authorship preserved (7dd945b on main): the electron-side (connectionId, profile) roster dedup, the plugin-boundary identity guard, and the canonical-chat adoption (reusing an existing CLI/A2A "Bot Chat" instead of minting a duplicate) all shipped as you wrote them, woven together with #88489's primary-source matching. Thanks for covering both halves of the duplication surface plus the session leak.

@teknium1 teknium1 closed this Aug 17, 2026
lisajlau pushed a commit to lisajlau/hermes-agent that referenced this pull request Aug 20, 2026
…source-qualified row keys

Layers on the salvaged NousResearch#88489 (@29206394) and NousResearch#88341 (@frizikk):

- sdk: host.activeConnectionId() — registry id of the LIVE active gateway.
  The salvaged fix classifies against the registry primary; after the user
  activates a non-primary source's agent, profiles.list answers from THAT
  source and primary-based matching would duplicate the active source's
  agents again. Live id wins, primaryConnectionId is the fallback, the
  legacy kind==='local' rule covers older desktops.
- plugin: roster/chip/picker list keys are botRowKey(bot) — source-qualified
  (connectionId, name) — so same-named agents on two genuine sources can
  never collide as duplicate React keys (the render half of the dupe-bots
  smear: name-keyed rows + duplicate names = repeated blocks every poll).
  Annotated active-source rows keep the plain-name key, so nothing remounts
  when a desktop gains the union roster.
- tests: live-id-beats-primary regression, botRowKey stability, source-shape
  anchor refresh.
bobaba76 pushed a commit to bobaba76/hermes-agent that referenced this pull request Aug 27, 2026
…source-qualified row keys

Layers on the salvaged NousResearch#88489 (@29206394) and NousResearch#88341 (@frizikk):

- sdk: host.activeConnectionId() — registry id of the LIVE active gateway.
  The salvaged fix classifies against the registry primary; after the user
  activates a non-primary source's agent, profiles.list answers from THAT
  source and primary-based matching would duplicate the active source's
  agents again. Live id wins, primaryConnectionId is the fallback, the
  legacy kind==='local' rule covers older desktops.
- plugin: roster/chip/picker list keys are botRowKey(bot) — source-qualified
  (connectionId, name) — so same-named agents on two genuine sources can
  never collide as duplicate React keys (the render half of the dupe-bots
  smear: name-keyed rows + duplicate names = repeated blocks every poll).
  Annotated active-source rows keep the plain-name key, so nothing remounts
  when a desktop gains the union roster.
- tests: live-id-beats-primary regression, botRowKey stability, source-shape
  anchor refresh.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…source-qualified row keys

Layers on the salvaged NousResearch#88489 (@29206394) and NousResearch#88341 (@frizikk):

- sdk: host.activeConnectionId() — registry id of the LIVE active gateway.
  The salvaged fix classifies against the registry primary; after the user
  activates a non-primary source's agent, profiles.list answers from THAT
  source and primary-based matching would duplicate the active source's
  agents again. Live id wins, primaryConnectionId is the fallback, the
  legacy kind==='local' rule covers older desktops.
- plugin: roster/chip/picker list keys are botRowKey(bot) — source-qualified
  (connectionId, name) — so same-named agents on two genuine sources can
  never collide as duplicate React keys (the render half of the dupe-bots
  smear: name-keyed rows + duplicate names = repeated blocks every poll).
  Annotated active-source rows keep the plain-name key, so nothing remounts
  when a desktop gains the union roster.
- tests: live-id-beats-primary regression, botRowKey stability, source-shape
  anchor refresh.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop Bots: duplicate roster identities create repeated @default-this-device rows

5 participants