NousResearch/hermes-agent

Desktop sidebar duplicate rows for compressed session lineages

Cluster 43483. Data as of

Nothing open on GitHub. Fix merged in #43487 on 11 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #43487. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #43487. That is separate from the fix event.
Still open
Nothing open on GitHub.

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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #43487 fix(desktop): deduplicate sidebar rows by compression lineage

    merged to main

    files: apps/desktop/src/store/session.test.ts, apps/desktop/src/store/session.ts

    Show the diff
    • apps/desktop/src/store/session.test.ts
      @@ -133,13 +133,52 @@ describe('mergeSessionPage', () => {
         it('keeps a pinned session matched by its lineage root after compression', () => {
           // The pin is stored on the lineage-root id, but the loaded row surfaces
           // under its live compression tip. Matching on _lineage_root_id keeps it.
      -    const previous = [session({ id: 'tip', _lineage_root_id: 'root' })]
      -    const incoming = [session({ id: 'other' })]
      +    const previous = [session({ id: 'tip', _lineage_root_id: 'root' })] as SessionInfo[]
      +    const incoming = [session({ id: 'other' })] as SessionInfo[]
       
           const merged = mergeSessionPage(previous, incoming, ['root'])
       
           expect(merged.map(s => s.id)).toEqual(['tip', 'other'])
         })
      +
      +  it('evicts an old compression tip when the incoming page has the new tip from the same lineage', () => {
      +    // Repro of #43483: after auto-compression rotates the tip (#4 → #5),
      +    // the sidebar showed both the old tip and the new tip as separate rows.
      +    // The old tip must be evicted because its lineage key matches the incoming
      +    // new tip's lineage key.
      +    const previous = [
      +      session({ id: 'tip-4', _lineage_root_id: 'root' }),
      +      session({ id: 'other' }),
      +    ] as SessionInfo[]
      +    const incoming = [
      +      session({ id: 'tip-5', _lineage_root_id: 'root' }),
      +    ] as SessionInfo[]
      +
      +    // 'tip-4' is in the keep set (e.g. it was the active/working session),
      +    // but should still be evicted because the incoming page carries the same
      +    // lineage under a new tip id.
      +    const merged = mergeSessionPage(previous, incoming, ['tip-4'])
      +
      +    expect(merged.map(s => s.id)).toEqual(['tip-5'])
      +    // The new tip comes from the server payload.
      +    expect(merged.find(s => s.id === 'tip-5')?._lineage_root_id).toBe('root')
      +  })
      +
      +  it('preserves an unrelated pinned session even when lineage dedup is active', () => {
      +    // Regression guard: lineage dedup must not accidentally evict sessions
      +    // from a different lineage that happen to be in the keep set.
      +    const previous = [
      +      session({ id: 'a-old', _lineage_root_id: 'lineage-a' }),
      +      session({ id: 'b', _lineage_root_id: 'lineage-b' }),
      +    ] as SessionInfo[]
      +    const incoming = [
      +      session({ id: 'a-new', _lineage_root_id: 'lineage-a' }),
      +    ] as SessionInfo[]
      +
      +    const merged = mergeSessionPage(previous, incoming, ['b'])
      +
      +    expect(merged.map(s => s.id)).toEqual(['b', 'a-new'])
      +  })
       })
       
       describe('workspaceCwdForNewSession', () => {
    • apps/desktop/src/store/session.ts
      @@ -125,10 +125,18 @@ export function mergeSessionPage(
         }
       
         const incomingIds = new Set(incoming.map(session => session.id))
      +  // Deduplicate by compression lineage: when auto-compression rotates the tip
      +  // id (old #4 → new #5), the incoming page carries the new tip but the
      +  // previous list still holds the old one.  Without lineage-level dedup both
      +  // rows survive as separate sidebar entries (fixes #43483).
      +  const incomingLineageKeys = new Set(
      +    incoming.map(session => session._lineage_root_id ?? session.id)
      +  )
       
         const survivors = previous.filter(
           session =>
             !incomingIds.has(session.id) &&
      +      !incomingLineageKeys.has(session._lineage_root_id ?? session.id) &&
             (keep.has(session.id) || (session._lineage_root_id != null && keep.has(session._lineage_root_id)))
         )
       

1 PR closed without merging

From the close comments: duplicate 1

  • #44069 fix(desktop): dedupe compressed session tips in sidebar merge · duplicate → #43487, #43483
Closed issues · 1
  • Closed#43483Desktop sidebar can show duplicate rows for the same auto-compressed session lineage
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#43487
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
11 Jun 2026

For your agent

The same facts, typed.