fix(desktop): stop duplicating active-gateway bots in the multi-source roster - #88489
Closed
29206394 wants to merge 1 commit into
Closed
fix(desktop): stop duplicating active-gateway bots in the multi-source roster#8848929206394 wants to merge 1 commit into
29206394 wants to merge 1 commit into
Conversation
…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
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.
Contributor
|
Merged via #88523 — your commit was cherry-picked with authorship preserved (1852092 on main). Your root-cause analysis was exactly right: |
This was referenced 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sprofiles.list(rich rows: ui_meta, last_session)union=host.agents()→buildAgentRoster(), which enumerates every registered connection, including the active gateway itselfmergeMultiSourceRoster()matched "rows that belong to the active gateway" withagent.connectionKind === 'local'. On a remote-primary desktop the active gateway reportsconnectionKind: '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:rosternow returnsprimaryConnectionId(the registry's primary, i.e. the connection that owns the active gateway).mergeMultiSourceRoster()matches union agents to the active gateway byconnectionId === primaryConnectionIdand annotates the local rows in place instead of appending duplicates.@name-devicedisambiguation rule is preserved.primaryConnectionIdis absent (older Electron builds), it falls back to the legacyconnectionKind === '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
How to Test
node --test src/plugins/hermes-bots/tests/multi-source-roster.test.mjs— 7 passednpm run check:test:plugins— 151 passednpx vitest run electron/connection-registry.test.ts— 43 passedManual 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-devicerow.Checklist
registry.primaryalready exists on the registry type).