{"repo":"NousResearch/hermes-agent","public_id":"48808","display_label":"Desktop OS notifications and session routing fixes","summary":"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.","subsystem":"apps/desktop","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":2,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":48999},"resolution":{"state":"resolved","by":"pr-48999,pr-107077"},"freshness":{"state":"verified","last_verified_at":"2026-09-10T04:37:49+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"fix_edges":[{"issue_number":48808,"pr_number":48999,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":51444,"pr_number":107077,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":65493,"pr_number":107077,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":100404,"pr_number":107077,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":48808,"title":"[Bug]: Hermes Desktop on Windows — clicking notification shows \"session not found\"","url":null,"state":"closed"},{"kind":"issue","number":65493,"title":"[Bug]: (Desktop)Clicking system notification should restore window and navigate to the corresponding session","url":null,"state":"closed"},{"kind":"pr","number":48831,"title":"fix(desktop): route notification clicks by stored session id","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48808","number":48808},{"kind":"pr","value":"#48999","number":48999},{"kind":"pr","value":"#48999","number":48999}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":48999,"title":"fix(desktop): resume stored session id on notification click","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/desktop-controller.tsx","apps/desktop/src/lib/session-ids.test.ts","apps/desktop/src/lib/session-ids.ts"],"fix_hunks":[{"file":"apps/desktop/src/app/desktop-controller.tsx","hunks":["@@ -14,6 +14,7 @@ import { useSkinCommand } from '@/themes/use-skin-command'\n import { formatRefValue } from '../components/assistant-ui/directive-text'\n import { getCronJobs, getSessionMessages, listAllProfileSessions, type SessionInfo, triggerCronJob } from '../hermes'\n import { type ChatMessage, chatMessageText, preserveLocalAssistantErrors, toChatMessages } from '../lib/chat-messages'\n+import { storedSessionIdForNotification } from '../lib/session-ids'\n import {\n   isMessagingSource,\n   LOCAL_SESSION_SOURCE_IDS,","@@ -276,16 +277,20 @@ export function DesktopController() {\n     }\n   }, [])\n \n-  // Notification click: the main process already focused the window; jump to its session.\n+  // Notification click: the main process already focused the window; jump to its\n+  // session. Notifications are tagged with the gateway *runtime* session id, but\n+  // the chat route is keyed by the *stored* id — navigating with the runtime id\n+  // resumes a non-existent stored session (\"session not found\") and strands the\n+  // user. Translate runtime -> stored before navigating.\n   useEffect(() => {\n     const unsubscribe = window.hermesDesktop?.onFocusSession?.(sessionId => {\n       if (sessionId) {\n-        navigate(sessionRoute(sessionId))\n+        navigate(sessionRoute(storedSessionIdForNotification(sessionId, runtimeIdByStoredSessionIdRef.current)))\n       }\n     })\n \n     return () => unsubscribe?.()\n-  }, [navigate])\n+  }, [navigate, runtimeIdByStoredSessionIdRef])\n \n   // Notification action button (Approve/Reject) — resolve in place, no navigation.\n   useEffect(() => {"]},{"file":"apps/desktop/src/lib/session-ids.test.ts","hunks":["@@ -0,0 +1,44 @@\n+import { describe, expect, it } from 'vitest'\n+\n+import { storedSessionIdForNotification } from './session-ids'\n+\n+describe('storedSessionIdForNotification', () => {\n+  it('translates a runtime id back to its stored id', () => {\n+    // The route is keyed by the stored id, but notifications carry the runtime\n+    // id. Resolving runtime -> stored keeps notification-click navigation from\n+    // resuming a non-existent stored session (\"session not found\").\n+    const map = new Map([['stored-abc', 'runtime-123']])\n+\n+    expect(storedSessionIdForNotification('runtime-123', map)).toBe('stored-abc')\n+  })\n+\n+  it('returns the id unchanged when no mapping is known', () => {\n+    // A notification for a session this window never opened may already carry a\n+    // stored id; let the resume/REST lookup handle it as-is.\n+    const map = new Map([['stored-abc', 'runtime-123']])\n+\n+    expect(storedSessionIdForNotification('stored-xyz', map)).toBe('stored-xyz')\n+  })\n+\n+  it('returns the id unchanged for an empty map', () => {\n+    expect(storedSessionIdForNotification('runtime-123', new Map())).toBe('runtime-123')\n+  })\n+\n+  it('resolves the correct stored id among several sessions', () => {\n+    const map = new Map([\n+      ['stored-1', 'runtime-1'],\n+      ['stored-2', 'runtime-2'],\n+      ['stored-3', 'runtime-3']\n+    ])\n+\n+    expect(storedSessionIdForNotification('runtime-2', map)).toBe('stored-2')\n+  })\n+\n+  it('does not treat a stored id as a runtime id (keys are not matched)', () => {\n+    // The map is stored -> runtime. A value that only appears as a *key* must\n+    // not be rewritten, otherwise an already-stored id could be mangled.\n+    const map = new Map([['stored-1', 'runtime-1']])\n+\n+    expect(storedSessionIdForNotification('stored-1', map)).toBe('stored-1')\n+  })\n+})"]},{"file":"apps/desktop/src/lib/session-ids.ts","hunks":["@@ -0,0 +1,26 @@\n+// The gateway tags every event — and therefore every native notification —\n+// with the *runtime* session id (the key under which the session lives in the\n+// gateway's in-memory `_sessions` map). The chat route, however, is keyed by\n+// the *stored* session id (`stored_session_id`), which is a different value:\n+// a brand-new chat gets a runtime id immediately but its stored id is assigned\n+// when the first turn persists. Navigating to a runtime id therefore tries to\n+// resume a stored session that does not exist (\"session not found\") and\n+// strands the user, who experiences it as the running session being destroyed.\n+//\n+// `runtimeIdByStoredSessionId` maps stored -> runtime; this resolves the\n+// reverse so notification-click navigation lands on the real route. The id is\n+// returned unchanged when no mapping is known — it may already be a stored id\n+// (e.g. a notification for a session this window never opened), in which case\n+// the normal resume/REST lookup handles it.\n+export function storedSessionIdForNotification(\n+  id: string,\n+  runtimeIdByStoredSessionId: ReadonlyMap<string, string>\n+): string {\n+  for (const [storedId, runtimeId] of runtimeIdByStoredSessionId) {\n+    if (runtimeId === id) {\n+      return storedId\n+    }\n+  }\n+\n+  return id\n+}\n"]}]},{"kind":"pr","number":107077,"title":"fix(desktop): make native notifications reveal their session tab","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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"]}],"trust":{"default_tier":"verified","includes_suggested":false}}