{"repo":"NousResearch/hermes-agent","public_id":"48504","display_label":"Desktop remote startup timeout fixes for profile loading","summary":"A bug and several fixes addressing cases where desktop remote startup fails due to insufficient API timeouts when fetching profiles. The changes extend timeouts and offload blocking profile endpoint calls to prevent event loop bottlenecks.","subsystem":"apps/desktop","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":57428,"merged_at":"2026-07-03T00:36:44+00:00"},"resolution":{"state":"resolved","by":"pr-57428"},"freshness":{"state":"verified","last_verified_at":"2026-07-03T00:36:44+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":48504,"pr_number":57428,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-03T00:36:44+00:00"}],"members":[{"kind":"issue","number":48504,"title":"[Bug]: Desktop remote startup fails when profile listing exceeds hardcoded 15s API timeout","url":null,"state":"closed"},{"kind":"pr","number":48526,"title":"fix(desktop): raise authenticated fetch timeout so profile-heavy remote startup doesn't time out (fixes #48504)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#57428","number":57428}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":57428,"title":"fix(desktop): extend startup long-timeout to the whole boot data burst (supersedes #48518, #48526)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/src/hermes.test.ts","apps/desktop/src/hermes.ts","apps/desktop/src/store/profile.ts"],"fix_hunks":[{"file":"apps/desktop/src/hermes.test.ts","hunks":["@@ -1,6 +1,18 @@\n import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\n \n-import { getSessionMessages, listAllProfileSessions, listSessions } from './hermes'\n+import {\n+  getCronJobs,\n+  getGlobalModelInfo,\n+  getGlobalModelOptions,\n+  getHermesConfig,\n+  getHermesConfigDefaults,\n+  getProfiles,\n+  getSessionMessages,\n+  getStatus,\n+  listAllProfileSessions,\n+  listSessions\n+} from './hermes'\n+import { refreshActiveProfile } from './store/profile'\n \n const emptySessionsResponse = {\n   limit: 0,","@@ -47,6 +59,73 @@ describe('Hermes REST session helpers', () => {\n     )\n   })\n \n+  it('uses a longer timeout for profile listing during desktop startup', async () => {\n+    api.mockResolvedValue({ profiles: [] })\n+\n+    await getProfiles()\n+\n+    expect(api).toHaveBeenCalledWith(\n+      expect.objectContaining({\n+        path: '/api/profiles',\n+        timeoutMs: 60_000\n+      })\n+    )\n+  })\n+\n+  it('uses a longer timeout for active profile refresh during desktop startup', async () => {\n+    api\n+      .mockResolvedValueOnce({ current: 'default' })\n+      .mockResolvedValueOnce({ profiles: [] })\n+\n+    await refreshActiveProfile()\n+\n+    expect(api).toHaveBeenNthCalledWith(\n+      1,\n+      expect.objectContaining({\n+        path: '/api/profiles/active',\n+        timeoutMs: 60_000\n+      })\n+    )\n+    expect(api).toHaveBeenNthCalledWith(\n+      2,\n+      expect.objectContaining({\n+        path: '/api/profiles',\n+        timeoutMs: 60_000\n+      })\n+    )\n+  })\n+\n+  it('gives the whole startup data burst the long timeout, not just profiles', async () => {\n+    api.mockResolvedValue({})\n+\n+    const bootCalls: [() => Promise<unknown>, string][] = [\n+      [getHermesConfig, '/api/config'],\n+      [getHermesConfigDefaults, '/api/config/defaults'],\n+      [getGlobalModelInfo, '/api/model/info'],\n+      [() => getGlobalModelOptions(), '/api/model/options'],\n+      [getCronJobs, '/api/cron/jobs']\n+    ]\n+\n+    for (const [call, path] of bootCalls) {\n+      api.mockClear()\n+      await call()\n+      expect(api).toHaveBeenCalledWith(expect.objectContaining({ path, timeoutMs: 60_000 }))\n+    }\n+  })\n+\n+  it('keeps the liveness poll on the short default so a dead backend fails fast', async () => {\n+    api.mockResolvedValue({})\n+    api.mockClear()\n+\n+    await getStatus()\n+\n+    // /api/status must NOT carry the long startup timeout — it is the runtime\n+    // liveness probe and has to fail quickly when the backend drops.\n+    const call = api.mock.calls[0]?.[0] as { path: string; timeoutMs?: number }\n+    expect(call.path).toBe('/api/status')\n+    expect(call.timeoutMs).toBeUndefined()\n+  })\n+\n   it('tags cross-profile message reads for Electron routing and backend lookup', async () => {\n     api.mockResolvedValue({ messages: [], session_id: 'session-1' })\n "]},{"file":"apps/desktop/src/hermes.ts","hunks":["@@ -47,6 +47,18 @@ import type {\n   ToolsetInfo\n } from '@/types/hermes'\n \n+// Desktop startup fires a burst of read-only data calls (config, profiles,\n+// model info/options, cron) the moment the backend passes readiness. On a\n+// profile-heavy or remote install these can each take tens of seconds — e.g.\n+// /api/profiles runs list_profiles(), which does a recursive skill-tree walk\n+// per profile — so the 15s default (DEFAULT_FETCH_TIMEOUT_MS in hardening.cjs)\n+// times out a backend that is alive-but-busy, surfacing as a spurious\n+// \"Timed out connecting to Hermes backend\" that hangs the UI (#48504).\n+//\n+// Give the boot burst a generous per-call timeout instead of raising the\n+// global default: interactive/runtime calls and the liveness poll (/api/status)\n+// keep the short default so a genuinely-dead backend is still detected fast.\n+export const STARTUP_REQUEST_TIMEOUT_MS = 60_000\n const DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS = 30_000\n const SESSION_LIST_REQUEST_TIMEOUT_MS = 60_000\n // prompt.submit is effectively fire-and-forget: turn completion is signaled by","@@ -287,7 +299,8 @@ export function renameSession(\n export function getGlobalModelInfo(): Promise<ModelInfoResponse> {\n   return window.hermesDesktop.api<ModelInfoResponse>({\n     ...profileScoped(),\n-    path: '/api/model/info'\n+    path: '/api/model/info',\n+    timeoutMs: STARTUP_REQUEST_TIMEOUT_MS\n   })\n }\n ","@@ -333,7 +346,8 @@ export function getLogs(params: {\n export function getHermesConfig(): Promise<HermesConfig> {\n   return window.hermesDesktop.api<HermesConfig>({\n     ...profileScoped(),\n-    path: '/api/config'\n+    path: '/api/config',\n+    timeoutMs: STARTUP_REQUEST_TIMEOUT_MS\n   })\n }\n ","@@ -347,7 +361,8 @@ export function getHermesConfigRecord(): Promise<HermesConfigRecord> {\n export function getHermesConfigDefaults(): Promise<HermesConfigRecord> {\n   return window.hermesDesktop.api<HermesConfigRecord>({\n     ...profileScoped(),\n-    path: '/api/config/defaults'\n+    path: '/api/config/defaults',\n+    timeoutMs: STARTUP_REQUEST_TIMEOUT_MS\n   })\n }\n ","@@ -638,7 +653,8 @@ export function testMessagingPlatform(platformId: string): Promise<MessagingPlat\n \n export function getCronJobs(): Promise<CronJob[]> {\n   return window.hermesDesktop.api<CronJob[]>({\n-    path: '/api/cron/jobs'\n+    path: '/api/cron/jobs',\n+    timeoutMs: STARTUP_REQUEST_TIMEOUT_MS\n   })\n }\n ","@@ -702,7 +718,8 @@ export function deleteCronJob(jobId: string): Promise<{ ok: boolean }> {\n \n export function getProfiles(): Promise<ProfilesResponse> {\n   return window.hermesDesktop.api<ProfilesResponse>({\n-    path: '/api/profiles'\n+    path: '/api/profiles',\n+    timeoutMs: STARTUP_REQUEST_TIMEOUT_MS\n   })\n }\n ","@@ -759,7 +776,8 @@ export function getUsageAnalytics(days = 30): Promise<AnalyticsResponse> {\n export function getGlobalModelOptions(opts?: { refresh?: boolean }): Promise<ModelOptionsResponse> {\n   return window.hermesDesktop.api<ModelOptionsResponse>({\n     ...profileScoped(),\n-    path: opts?.refresh ? '/api/model/options?refresh=1' : '/api/model/options'\n+    path: opts?.refresh ? '/api/model/options?refresh=1' : '/api/model/options',\n+    timeoutMs: STARTUP_REQUEST_TIMEOUT_MS\n   })\n }\n "]},{"file":"apps/desktop/src/store/profile.ts","hunks":["@@ -1,6 +1,6 @@\n import { atom, computed } from 'nanostores'\n \n-import { getProfiles, setApiRequestProfile } from '@/hermes'\n+import { getProfiles, setApiRequestProfile, STARTUP_REQUEST_TIMEOUT_MS } from '@/hermes'\n import { queryClient } from '@/lib/query-client'\n import {\n   arraysEqual,","@@ -110,7 +110,10 @@ interface ActiveProfileResponse {\n // Best-effort: failures (backend not up yet) leave the prior values intact.\n export async function refreshActiveProfile(): Promise<void> {\n   try {\n-    const res = await window.hermesDesktop.api<ActiveProfileResponse>({ path: '/api/profiles/active' })\n+    const res = await window.hermesDesktop.api<ActiveProfileResponse>({\n+      path: '/api/profiles/active',\n+      timeoutMs: STARTUP_REQUEST_TIMEOUT_MS\n+    })\n \n     setActiveProfile(res.current || 'default')\n   } catch {\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}