{"repo":"NousResearch/hermes-agent","public_id":"43483","display_label":"Desktop sidebar duplicate rows for compressed session lineages","summary":"One issue reports that the desktop sidebar can show duplicate rows for the same auto-compressed session lineage. Two pull requests propose deduplicating those sidebar rows, by compression lineage and by compressed session tips during merge; the titles alone do not show their status or outcome.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":43487,"merged_at":"2026-06-11T08:02:28+00:00"},"resolution":{"state":"resolved","by":"pr-43487"},"freshness":{"state":"verified","last_verified_at":"2026-06-11T08:02:28+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":43483,"pr_number":43487,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-11T08:02:28+00:00"}],"members":[{"kind":"issue","number":43483,"title":"Desktop sidebar can show duplicate rows for the same auto-compressed session lineage","url":null,"state":"closed"},{"kind":"pr","number":43487,"title":"fix(desktop): deduplicate sidebar rows by compression lineage","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/src/store/session.test.ts","apps/desktop/src/store/session.ts"],"fix_hunks":[{"file":"apps/desktop/src/store/session.test.ts","hunks":["@@ -133,13 +133,52 @@ describe('mergeSessionPage', () => {\n   it('keeps a pinned session matched by its lineage root after compression', () => {\n     // The pin is stored on the lineage-root id, but the loaded row surfaces\n     // under its live compression tip. Matching on _lineage_root_id keeps it.\n-    const previous = [session({ id: 'tip', _lineage_root_id: 'root' })]\n-    const incoming = [session({ id: 'other' })]\n+    const previous = [session({ id: 'tip', _lineage_root_id: 'root' })] as SessionInfo[]\n+    const incoming = [session({ id: 'other' })] as SessionInfo[]\n \n     const merged = mergeSessionPage(previous, incoming, ['root'])\n \n     expect(merged.map(s => s.id)).toEqual(['tip', 'other'])\n   })\n+\n+  it('evicts an old compression tip when the incoming page has the new tip from the same lineage', () => {\n+    // Repro of #43483: after auto-compression rotates the tip (#4 → #5),\n+    // the sidebar showed both the old tip and the new tip as separate rows.\n+    // The old tip must be evicted because its lineage key matches the incoming\n+    // new tip's lineage key.\n+    const previous = [\n+      session({ id: 'tip-4', _lineage_root_id: 'root' }),\n+      session({ id: 'other' }),\n+    ] as SessionInfo[]\n+    const incoming = [\n+      session({ id: 'tip-5', _lineage_root_id: 'root' }),\n+    ] as SessionInfo[]\n+\n+    // 'tip-4' is in the keep set (e.g. it was the active/working session),\n+    // but should still be evicted because the incoming page carries the same\n+    // lineage under a new tip id.\n+    const merged = mergeSessionPage(previous, incoming, ['tip-4'])\n+\n+    expect(merged.map(s => s.id)).toEqual(['tip-5'])\n+    // The new tip comes from the server payload.\n+    expect(merged.find(s => s.id === 'tip-5')?._lineage_root_id).toBe('root')\n+  })\n+\n+  it('preserves an unrelated pinned session even when lineage dedup is active', () => {\n+    // Regression guard: lineage dedup must not accidentally evict sessions\n+    // from a different lineage that happen to be in the keep set.\n+    const previous = [\n+      session({ id: 'a-old', _lineage_root_id: 'lineage-a' }),\n+      session({ id: 'b', _lineage_root_id: 'lineage-b' }),\n+    ] as SessionInfo[]\n+    const incoming = [\n+      session({ id: 'a-new', _lineage_root_id: 'lineage-a' }),\n+    ] as SessionInfo[]\n+\n+    const merged = mergeSessionPage(previous, incoming, ['b'])\n+\n+    expect(merged.map(s => s.id)).toEqual(['b', 'a-new'])\n+  })\n })\n \n describe('workspaceCwdForNewSession', () => {"]},{"file":"apps/desktop/src/store/session.ts","hunks":["@@ -125,10 +125,18 @@ export function mergeSessionPage(\n   }\n \n   const incomingIds = new Set(incoming.map(session => session.id))\n+  // Deduplicate by compression lineage: when auto-compression rotates the tip\n+  // id (old #4 → new #5), the incoming page carries the new tip but the\n+  // previous list still holds the old one.  Without lineage-level dedup both\n+  // rows survive as separate sidebar entries (fixes #43483).\n+  const incomingLineageKeys = new Set(\n+    incoming.map(session => session._lineage_root_id ?? session.id)\n+  )\n \n   const survivors = previous.filter(\n     session =>\n       !incomingIds.has(session.id) &&\n+      !incomingLineageKeys.has(session._lineage_root_id ?? session.id) &&\n       (keep.has(session.id) || (session._lineage_root_id != null && keep.has(session._lineage_root_id)))\n   )\n \n"]}]},{"kind":"pr","number":44069,"title":"fix(desktop): dedupe compressed session tips in sidebar merge","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#43487","number":43487},{"kind":"pr","value":"#43483","number":43483}],"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}