mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
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:
parent
584d3ae532
commit
4aad27b751
3 changed files with 62 additions and 9 deletions
|
|
@ -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' })
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue