{"repo":"NousResearch/hermes-agent","public_id":"10374","display_label":"Session compression lineage tracking and deduplication fixes","summary":"These issues and fixes center on how compressed session lineages are identified, listed, and displayed — covering bugs where desktop shows duplicate sidebar rows, hides compressed sessions, loses transcripts, and where gateway RPCs and state fail to propagate lineage root IDs correctly. Several fixes address deleting or preserving full lineages rather than only visible tips.","subsystem":"agent","counts":{"members":18,"open_issues":4,"open_issues_distinct":4,"open_prs":0,"closed_issues":4,"merged_prs":3,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":43487},"resolution":{"state":"partially_resolved","by":"pr-43487,pr-86595,pr-134061"},"freshness":{"state":"verified","last_verified_at":"2026-10-06T21:03:27+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.08183006535947712},"fix_edges":[{"issue_number":43483,"pr_number":43487,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":66663,"pr_number":134061,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":67871,"pr_number":86595,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":80680,"pr_number":86595,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":10374,"title":"Feature: lineage-aware /resume core for CLI and gateway","url":null,"state":"open"},{"kind":"issue","number":43483,"title":"Desktop sidebar can show duplicate rows for the same auto-compressed session lineage","url":null,"state":"closed"},{"kind":"issue","number":44913,"title":"Desktop hides compressed session lineage and can spin forever while response exists in gateway dashboard","url":null,"state":"open"},{"kind":"issue","number":48524,"title":"fix(sessions): delete entire compression lineages, not just the visible tip","url":null,"state":"open"},{"kind":"issue","number":50192,"title":"Desktop should treat compressed session lineages as one logical chat","url":null,"state":"open"},{"kind":"issue","number":66663,"title":"[gateway] session.list RPC drops _lineage_root_id, breaking desktop session dedup","url":null,"state":"closed"},{"kind":"issue","number":66664,"title":"[gateway] _lineage_root_id not set on uncompressed tips, breaking desktop lineage dedup","url":null,"state":"closed"},{"kind":"issue","number":67871,"title":"[Bug]: Running compressed Desktop sessions replace the complete transcript","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","implication":"superseded"},"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":"#43487","number":43487},{"kind":"pr","value":"#43483","number":43483}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":45084,"title":"fix: Desktop hides compressed session lineage and can spin forever while response exists in gateway dashboard (#44913)","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":48525,"title":"fix(sessions): delete entire compression lineages, not just the visible tip","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7b26530dfe","number":0},{"kind":"commit","value":"aaf7f199d5","number":0},{"kind":"commit","value":"4f64356f5e","number":0},{"kind":"commit","value":"ecac27c0af","number":0}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":48634,"title":"fix(state): delete entire compression lineages (#48524)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48525","number":48525},{"kind":"pr","value":"#48524","number":48524},{"kind":"pr","value":"#48525","number":48525},{"kind":"pr","value":"#48525","number":48525}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":66675,"title":"fix(gateway): preserve session lineage in list RPC","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":66728,"title":"fix(state): set _lineage_root_id on uncompressed sessions for desktop dedup (#66664)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#66664","number":66664}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":67879,"title":"fix(desktop): preserve full transcripts for running sessions","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#86595","number":86595}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":86595,"title":"fix(desktop): salvage compaction transcript-visibility cluster — surface archived turns + preserve running-session transcripts (#67871)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/src/app/session/hooks/use-session-actions.test.tsx","apps/desktop/src/app/session/hooks/use-session-actions/index.ts","apps/desktop/src/app/session/hooks/use-session-actions/utils.test.ts","apps/desktop/src/app/session/hooks/use-session-actions/utils.ts","apps/desktop/src/hermes.test.ts","apps/desktop/src/hermes.ts","hermes_cli/web_routers/sessions.py","hermes_state.py","tests/hermes_cli/test_web_server.py","tests/hermes_state/test_get_messages_include_compacted.py"]},{"kind":"pr","number":134061,"title":"fix(gateway): preserve session lineage in the session.list RPC","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/shared/src/gateway-contract.generated.ts","apps/shared/src/gateway-contract.openrpc.json","tests/tui_gateway/test_session_list_lineage_rpc.py","tui_gateway/contracts/sessions.py","tui_gateway/methods_session.py","tui_gateway/methods_session_branch.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}