fix(desktop): extend startup long-timeout to the whole boot data burst

Broadens Tranquil-Flow's profile-startup timeout fix (#48518) from getProfiles
+ refreshActiveProfile to the rest of the calls the desktop fires during
connect: /api/config, /api/config/defaults, /api/model/info, /api/model/options,
/api/cron/jobs. On a profile-heavy or remote install any of these can exceed
the 15s DEFAULT_FETCH_TIMEOUT_MS while the backend is alive-but-busy (e.g.
list_profiles walks the skill tree per profile), surfacing as the spurious
"Timed out connecting to Hermes backend after 15000ms" that hangs the UI
(#48504).

Uses the surgical per-call mechanism (renamed STARTUP_PROFILE_REQUEST_TIMEOUT_MS
→ STARTUP_REQUEST_TIMEOUT_MS) rather than raising the global default (the
alternative in #48526): the liveness poll /api/status and all interactive/
runtime calls keep the short default, so a genuinely-dead backend is still
detected fast and the boot readiness probe (waitForHermes) is untouched.

Supersedes #48518 (carried as the base commit) and #48526 (global-default
raise). Fixes #48504.

Co-authored-by: YapBi <129007007+HeLLGURD@users.noreply.github.com>
Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson 2026-07-02 19:34:53 -05:00 committed by brooklyn!
parent 584d3ae532
commit 4aad27b751
3 changed files with 62 additions and 9 deletions

View file

@ -1,8 +1,14 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
getCronJobs,
getGlobalModelInfo,
getGlobalModelOptions,
getHermesConfig,
getHermesConfigDefaults,
getProfiles,
getSessionMessages,
getStatus,
listAllProfileSessions,
listSessions
} from './hermes'
@ -89,6 +95,37 @@ describe('Hermes REST session helpers', () => {
)
})
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' })

View file

@ -47,7 +47,18 @@ import type {
ToolsetInfo
} from '@/types/hermes'
export const STARTUP_PROFILE_REQUEST_TIMEOUT_MS = 60_000
// 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
@ -288,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
})
}
@ -334,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
})
}
@ -348,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
})
}
@ -639,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
})
}
@ -704,7 +719,7 @@ export function deleteCronJob(jobId: string): Promise<{ ok: boolean }> {
export function getProfiles(): Promise<ProfilesResponse> {
return window.hermesDesktop.api<ProfilesResponse>({
path: '/api/profiles',
timeoutMs: STARTUP_PROFILE_REQUEST_TIMEOUT_MS
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
})
}
@ -761,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
})
}

View file

@ -1,6 +1,6 @@
import { atom, computed } from 'nanostores'
import { getProfiles, setApiRequestProfile, STARTUP_PROFILE_REQUEST_TIMEOUT_MS } from '@/hermes'
import { getProfiles, setApiRequestProfile, STARTUP_REQUEST_TIMEOUT_MS } from '@/hermes'
import { queryClient } from '@/lib/query-client'
import {
arraysEqual,
@ -112,7 +112,7 @@ export async function refreshActiveProfile(): Promise<void> {
try {
const res = await window.hermesDesktop.api<ActiveProfileResponse>({
path: '/api/profiles/active',
timeoutMs: STARTUP_PROFILE_REQUEST_TIMEOUT_MS
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
})
setActiveProfile(res.current || 'default')