Desktop OS notifications and session routing fixes

NousResearch/hermes-agent · cluster 48808 · apps/desktop

Data as of

Nothing open on GitHub. Fix merged in #48999. Check it covers your case.

The short answer

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

Work on native OS notification support for the desktop app, including per-type toggles and fixes for correctly resuming or navigating to the corresponding session when a notification is clicked. Includes bug reports about session-not-found errors and window restoration on notification click.

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.

  • #48999 fix(desktop): resume stored session id on notification click

    merged to main

    files: apps/desktop/src/app/desktop-controller.tsx, apps/desktop/src/lib/session-ids.test.ts, apps/desktop/src/lib/session-ids.ts

    Show the diff
    • apps/desktop/src/app/desktop-controller.tsx
      @@ -14,6 +14,7 @@ import { useSkinCommand } from '@/themes/use-skin-command'
       import { formatRefValue } from '../components/assistant-ui/directive-text'
       import { getCronJobs, getSessionMessages, listAllProfileSessions, type SessionInfo, triggerCronJob } from '../hermes'
       import { type ChatMessage, chatMessageText, preserveLocalAssistantErrors, toChatMessages } from '../lib/chat-messages'
      +import { storedSessionIdForNotification } from '../lib/session-ids'
       import {
         isMessagingSource,
         LOCAL_SESSION_SOURCE_IDS,
      @@ -276,16 +277,20 @@ export function DesktopController() {
           }
         }, [])
       
      -  // Notification click: the main process already focused the window; jump to its session.
      +  // Notification click: the main process already focused the window; jump to its
      +  // session. Notifications are tagged with the gateway *runtime* session id, but
      +  // the chat route is keyed by the *stored* id — navigating with the runtime id
      +  // resumes a non-existent stored session ("session not found") and strands the
      +  // user. Translate runtime -> stored before navigating.
         useEffect(() => {
           const unsubscribe = window.hermesDesktop?.onFocusSession?.(sessionId => {
             if (sessionId) {
      -        navigate(sessionRoute(sessionId))
      +        navigate(sessionRoute(storedSessionIdForNotification(sessionId, runtimeIdByStoredSessionIdRef.current)))
             }
           })
       
           return () => unsubscribe?.()
      -  }, [navigate])
      +  }, [navigate, runtimeIdByStoredSessionIdRef])
       
         // Notification action button (Approve/Reject) — resolve in place, no navigation.
         useEffect(() => {
    • apps/desktop/src/lib/session-ids.test.ts
      @@ -0,0 +1,44 @@
      +import { describe, expect, it } from 'vitest'
      +
      +import { storedSessionIdForNotification } from './session-ids'
      +
      +describe('storedSessionIdForNotification', () => {
      +  it('translates a runtime id back to its stored id', () => {
      +    // The route is keyed by the stored id, but notifications carry the runtime
      +    // id. Resolving runtime -> stored keeps notification-click navigation from
      +    // resuming a non-existent stored session ("session not found").
      +    const map = new Map([['stored-abc', 'runtime-123']])
      +
      +    expect(storedSessionIdForNotification('runtime-123', map)).toBe('stored-abc')
      +  })
      +
      +  it('returns the id unchanged when no mapping is known', () => {
      +    // A notification for a session this window never opened may already carry a
      +    // stored id; let the resume/REST lookup handle it as-is.
      +    const map = new Map([['stored-abc', 'runtime-123']])
      +
      +    expect(storedSessionIdForNotification('stored-xyz', map)).toBe('stored-xyz')
      +  })
      +
      +  it('returns the id unchanged for an empty map', () => {
      +    expect(storedSessionIdForNotification('runtime-123', new Map())).toBe('runtime-123')
      +  })
      +
      +  it('resolves the correct stored id among several sessions', () => {
      +    const map = new Map([
      +      ['stored-1', 'runtime-1'],
      +      ['stored-2', 'runtime-2'],
      +      ['stored-3', 'runtime-3']
      +    ])
      +
      +    expect(storedSessionIdForNotification('runtime-2', map)).toBe('stored-2')
      +  })
      +
      +  it('does not treat a stored id as a runtime id (keys are not matched)', () => {
      +    // The map is stored -> runtime. A value that only appears as a *key* must
      +    // not be rewritten, otherwise an already-stored id could be mangled.
      +    const map = new Map([['stored-1', 'runtime-1']])
      +
      +    expect(storedSessionIdForNotification('stored-1', map)).toBe('stored-1')
      +  })
      +})
    • apps/desktop/src/lib/session-ids.ts
      @@ -0,0 +1,26 @@
      +// The gateway tags every event — and therefore every native notification —
      +// with the *runtime* session id (the key under which the session lives in the
      +// gateway's in-memory `_sessions` map). The chat route, however, is keyed by
      +// the *stored* session id (`stored_session_id`), which is a different value:
      +// a brand-new chat gets a runtime id immediately but its stored id is assigned
      +// when the first turn persists. Navigating to a runtime id therefore tries to
      +// resume a stored session that does not exist ("session not found") and
      +// strands the user, who experiences it as the running session being destroyed.
      +//
      +// `runtimeIdByStoredSessionId` maps stored -> runtime; this resolves the
      +// reverse so notification-click navigation lands on the real route. The id is
      +// returned unchanged when no mapping is known — it may already be a stored id
      +// (e.g. a notification for a session this window never opened), in which case
      +// the normal resume/REST lookup handles it.
      +export function storedSessionIdForNotification(
      +  id: string,
      +  runtimeIdByStoredSessionId: ReadonlyMap<string, string>
      +): string {
      +  for (const [storedId, runtimeId] of runtimeIdByStoredSessionId) {
      +    if (runtimeId === id) {
      +      return storedId
      +    }
      +  }
      +
      +  return id
      +}
  • #107077 fix(desktop): make native notifications reveal their session tab

    merged to main

    files: apps/desktop/electron/main.ts, apps/desktop/electron/notification-actions.test.ts, apps/desktop/electron/notification-actions.ts, apps/desktop/electron/notification-ipc.test.ts, apps/desktop/electron/notification-ipc.ts, apps/desktop/electron/notification-registry.test.ts, apps/desktop/electron/notification-registry.ts, apps/desktop/electron/notification-types.ts, apps/desktop/src/app/contrib/hooks/notification-focus.test.tsx, apps/desktop/src/app/contrib/hooks/use-desktop-integrations.test.tsx, apps/desktop/src/app/contrib/hooks/use-desktop-integrations.ts, apps/desktop/src/app/open-session.test.ts, apps/desktop/src/app/open-session.ts, apps/desktop/src/global.d.ts, apps/desktop/src/store/native-notifications.test.ts, apps/desktop/src/store/native-notifications.ts

1 PR closed without merging

From the close comments: other 1

  • #48831 fix(desktop): route notification clicks by stored session id · other → #48808, #48999
Closed issues · 2
  • Closed#48808[Bug]: Hermes Desktop on Windows — clicking notification shows "session not found"
  • Closed#65493[Bug]: (Desktop)Clicking system notification should restore window and navigate to the corresponding session
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.34: 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

Subsystem
apps/desktop
Merged PRs
#48999, #107077
Closed issues
2
Attempts
1 PR closed without merging
Fix link checked
10 Sep 2026

For your agent

The same facts, typed.