Skip to content

fix(desktop): stop duplicating active-gateway bots in the multi-source roster - #88489

Closed
29206394 wants to merge 1 commit into
NousResearch:mainfrom
29206394:fix/bots-roster-primary-connection
Closed

fix(desktop): stop duplicating active-gateway bots in the multi-source roster#88489
29206394 wants to merge 1 commit into
NousResearch:mainfrom
29206394:fix/bots-roster-primary-connection

Conversation

@29206394

Copy link
Copy Markdown

What does this PR do?

Fixes the Desktop BOTS pane listing every bot twice (and more on each refetch) when the desktop's only backend is a remote gateway (#88344).

Root cause

useRoster() merges two sources of truth:

  • local = the active gateway's profiles.list (rich rows: ui_meta, last_session)
  • union = host.agents()buildAgentRoster(), which enumerates every registered connection, including the active gateway itself

mergeMultiSourceRoster() matched "rows that belong to the active gateway" with agent.connectionKind === 'local'. On a remote-primary desktop the active gateway reports connectionKind: 'remote', so its own agents fell into the "other source" branch and were appended as phantom duplicates — every bot ×2 at baseline, and the roster kept accumulating copies across refetch/repaint cycles.

Fix

  • hermes:agents:roster now returns primaryConnectionId (the registry's primary, i.e. the connection that owns the active gateway).
  • mergeMultiSourceRoster() matches union agents to the active gateway by connectionId === primaryConnectionId and annotates the local rows in place instead of appending duplicates.
  • Same-named profiles on genuinely separate sources (This device, other remotes) still get their own tagged rows — the @name-device disambiguation rule is preserved.
  • When primaryConnectionId is absent (older Electron builds), it falls back to the legacy connectionKind === 'local' rule, so single-source behavior stays byte-identical.

Related Issue

Fixes #88344

Complementary to #88341 (dedupes within the union and reuses canonical chats; this PR dedupes the active gateway across the local/union boundary).

Type of Change

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

How to Test

  1. node --test src/plugins/hermes-bots/tests/multi-source-roster.test.mjs — 7 passed
  2. npm run check:test:plugins — 151 passed
  3. npx vitest run electron/connection-registry.test.ts — 43 passed

Manual scenario: remote-only desktop (e.g. connect Desktop to a headless gateway, no local profiles). Open BOTS → every bot appears once; interacting with the app no longer multiplies rows. A local source with the same profile name still shows as a separate @name-device row.

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 (project-wide typecheck not run; change is additive and type-local — registry.primary already exists on the registry type).
  • I searched existing GitHub issues before opening the focused issue above.
  • I kept the change scoped to the BOTS roster merge boundary.
  • I manually exercised the packaged Desktop UI (automated coverage only).

…e roster

The union agent roster (host.agents) enumerates EVERY registered connection,
including the active gateway that already answered profiles.list. The plugin
merger treated the active gateway's own agents as rows from other sources
because a remote-primary desktop reports them with connectionKind 'remote',
so every bot appeared twice (baseline) and kept growing with each refetch.

Match union agents to the active gateway via the new primaryConnectionId
field on the roster RPC response and annotate the local rows in place
instead of appending phantom copies. Same-named profiles on genuinely
separate sources (This device, other remotes) still get their own tagged
rows, preserving the @name-device disambiguation rule.

Fall back to the legacy connectionKind==='local' rule when
primaryConnectionId is absent (older Electron builds), so single-source
behavior is byte-identical.

Fixes NousResearch#88344
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) labels Aug 17, 2026
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 (1852092 on main). Your root-cause analysis was exactly right: connectionKind === 'local' misclassified the active remote gateway's own agents, and your primaryConnectionId matching is the shipped base of the fix. #88523 layered a live host.activeConnectionId() override on top (covers the activated-non-primary-source state) plus source-qualified React row keys, and wove in #88341's identity dedup. Thanks for the fast, well-tested fix — first submitter of the root cause.

@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.
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 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.
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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bots pane duplicates every bot on remote-gateway-only desktops; rows multiply on each interaction

3 participants