NousResearch/hermes-agent

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

0open issues
0open PRs
3items grouped
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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #57428 fix(desktop): extend startup long-timeout to the whole boot data burst (supersedes #48518, #48526)

    merged to main

    files: apps/desktop/src/hermes.test.ts, apps/desktop/src/hermes.ts, apps/desktop/src/store/profile.ts

    Show 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.00 means 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 Lab

At a glance

Subsystem
apps/desktop
Merged PRs
#57428
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
3 Jul 2026

For your agent

The same facts, typed.