Desktop remote startup timeout fixes for profile loading
Cluster 48504 · apps/desktop. Data as of
Nothing open on GitHub. Fix merged in #57428 on 3 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #57428. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #57428. That is separate from the fix event. - Still open
- Nothing open on GitHub.
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.
Open issues0
Open PRs0
Fixes on record
-
#57428 fix(desktop): extend startup long-timeout to the whole boot data burst (supersedes #48518, #48526)
files:
apps/desktop/src/hermes.test.ts,apps/desktop/src/hermes.ts,apps/desktop/src/store/profile.tsShow the diff
-
apps/desktop/src/hermes.test.ts@@ -1,6 +1,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { getSessionMessages, listAllProfileSessions, listSessions } from './hermes' +import { + getCronJobs, + getGlobalModelInfo, + getGlobalModelOptions, + getHermesConfig, + getHermesConfigDefaults, + getProfiles, + getSessionMessages, + getStatus, + listAllProfileSessions, + listSessions +} from './hermes' +import { refreshActiveProfile } from './store/profile' const emptySessionsResponse = { limit: 0, @@ -47,6 +59,73 @@ describe('Hermes REST session helpers', () => { ) }) + it('uses a longer timeout for profile listing during desktop startup', async () => { + api.mockResolvedValue({ profiles: [] }) + + await getProfiles() + + expect(api).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/profiles', + timeoutMs: 60_000 + }) + ) + }) + + it('uses a longer timeout for active profile refresh during desktop startup', async () => { + api + .mockResolvedValueOnce({ current: 'default' }) + .mockResolvedValueOnce({ profiles: [] }) + + await refreshActiveProfile() + + expect(api).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + path: '/api/profiles/active', + timeoutMs: 60_000 + }) + ) + expect(api).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + path: '/api/profiles', + timeoutMs: 60_000 + }) + ) + }) + + it('gives the whole startup data burst the long timeout, not just profiles', async () => { + api.mockResolvedValue({}) + + const bootCalls: [() => Promise<unknown>, string][] = [ + [getHermesConfig, '/api/config'], + [getHermesConfigDefaults, '/api/config/defaults'], + [getGlobalModelInfo, '/api/model/info'], + [() => getGlobalModelOptions(), '/api/model/options'], + [getCronJobs, '/api/cron/jobs'] + ] + + for (const [call, path] of bootCalls) { + api.mockClear() + await call() + expect(api).toHaveBeenCalledWith(expect.objectContaining({ path, timeoutMs: 60_000 })) + } + }) + + it('keeps the liveness poll on the short default so a dead backend fails fast', async () => { + api.mockResolvedValue({}) + api.mockClear() + + await getStatus() + + // /api/status must NOT carry the long startup timeout — it is the runtime + // liveness probe and has to fail quickly when the backend drops. + const call = api.mock.calls[0]?.[0] as { path: string; timeoutMs?: number } + expect(call.path).toBe('/api/status') + expect(call.timeoutMs).toBeUndefined() + }) + it('tags cross-profile message reads for Electron routing and backend lookup', async () => { api.mockResolvedValue({ messages: [], session_id: 'session-1' }) -
apps/desktop/src/hermes.ts@@ -47,6 +47,18 @@ import type { ToolsetInfo } from '@/types/hermes' +// Desktop startup fires a burst of read-only data calls (config, profiles, +// model info/options, cron) the moment the backend passes readiness. On a +// profile-heavy or remote install these can each take tens of seconds — e.g. +// /api/profiles runs list_profiles(), which does a recursive skill-tree walk +// per profile — so the 15s default (DEFAULT_FETCH_TIMEOUT_MS in hardening.cjs) +// times out a backend that is alive-but-busy, surfacing as a spurious +// "Timed out connecting to Hermes backend" that hangs the UI (#48504). +// +// Give the boot burst a generous per-call timeout instead of raising the +// global default: interactive/runtime calls and the liveness poll (/api/status) +// keep the short default so a genuinely-dead backend is still detected fast. +export const STARTUP_REQUEST_TIMEOUT_MS = 60_000 const DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS = 30_000 const SESSION_LIST_REQUEST_TIMEOUT_MS = 60_000 // prompt.submit is effectively fire-and-forget: turn completion is signaled by @@ -287,7 +299,8 @@ export function renameSession( export function getGlobalModelInfo(): Promise<ModelInfoResponse> { return window.hermesDesktop.api<ModelInfoResponse>({ ...profileScoped(), - path: '/api/model/info' + path: '/api/model/info', + timeoutMs: STARTUP_REQUEST_TIMEOUT_MS }) } @@ -333,7 +346,8 @@ export function getLogs(params: { export function getHermesConfig(): Promise<HermesConfig> { return window.hermesDesktop.api<HermesConfig>({ ...profileScoped(), - path: '/api/config' + path: '/api/config', + timeoutMs: STARTUP_REQUEST_TIMEOUT_MS }) } @@ -347,7 +361,8 @@ export function getHermesConfigRecord(): Promise<HermesConfigRecord> { export function getHermesConfigDefaults(): Promise<HermesConfigRecord> { return window.hermesDesktop.api<HermesConfigRecord>({ ...profileScoped(), - path: '/api/config/defaults' + path: '/api/config/defaults', + timeoutMs: STARTUP_REQUEST_TIMEOUT_MS }) } @@ -638,7 +653,8 @@ export function testMessagingPlatform(platformId: string): Promise<MessagingPlat export function getCronJobs(): Promise<CronJob[]> { return window.hermesDesktop.api<CronJob[]>({ - path: '/api/cron/jobs' + path: '/api/cron/jobs', + timeoutMs: STARTUP_REQUEST_TIMEOUT_MS }) } @@ -702,7 +718,8 @@ export function deleteCronJob(jobId: string): Promise<{ ok: boolean }> { export function getProfiles(): Promise<ProfilesResponse> { return window.hermesDesktop.api<ProfilesResponse>({ - path: '/api/profiles' + path: '/api/profiles', + timeoutMs: STARTUP_REQUEST_TIMEOUT_MS }) } @@ -759,7 +776,8 @@ export function getUsageAnalytics(days = 30): Promise<AnalyticsResponse> { export function getGlobalModelOptions(opts?: { refresh?: boolean }): Promise<ModelOptionsResponse> { return window.hermesDesktop.api<ModelOptionsResponse>({ ...profileScoped(), - path: opts?.refresh ? '/api/model/options?refresh=1' : '/api/model/options' + path: opts?.refresh ? '/api/model/options?refresh=1' : '/api/model/options', + timeoutMs: STARTUP_REQUEST_TIMEOUT_MS }) } -
apps/desktop/src/store/profile.ts@@ -1,6 +1,6 @@ import { atom, computed } from 'nanostores' -import { getProfiles, setApiRequestProfile } from '@/hermes' +import { getProfiles, setApiRequestProfile, STARTUP_REQUEST_TIMEOUT_MS } from '@/hermes' import { queryClient } from '@/lib/query-client' import { arraysEqual, @@ -110,7 +110,10 @@ interface ActiveProfileResponse { // Best-effort: failures (backend not up yet) leave the prior values intact. export async function refreshActiveProfile(): Promise<void> { try { - const res = await window.hermesDesktop.api<ActiveProfileResponse>({ path: '/api/profiles/active' }) + const res = await window.hermesDesktop.api<ActiveProfileResponse>({ + path: '/api/profiles/active', + timeoutMs: STARTUP_REQUEST_TIMEOUT_MS + }) setActiveProfile(res.current || 'default') } catch {
-
1 PR closed without merging
From the close comments: superseded by another PR 1
-
#48526 fix(desktop): raise authenticated fetch timeout so profile-heavy remote startup doesn't time out (fixes #48504) · superseded by another PR →
#57428
Closed issues · 1
- Closed#48504[Bug]: Desktop remote startup fails when profile listing exceeds hardcoded 15s API timeout
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.00means 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 LabAt a glance
- Subsystem
- apps/desktop
- Merged PRs
- #57428
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 3 Jul 2026