# NousResearch/hermes-agent · Desktop remote startup timeout fixes for profile loading

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #57428. Verify it addresses your case.
- **Recommended:** read PR #57428 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-57428`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member 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.

## Closed / duplicate issues

- #48504: \[Bug\]: Desktop remote startup fails when profile listing exceeds hardcoded 15s API timeout

## Prior & competing PRs

- **#57428: fix(desktop): extend startup long-timeout to the whole boot data burst (supersedes #48518, #48526)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `apps/desktop/src/hermes.test.ts`, `apps/desktop/src/hermes.ts`, `apps/desktop/src/store/profile.ts`
  - fix hunks:
  - `apps/desktop/src/hermes.test.ts`

    ```diff
    @@ -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,
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
       })
     }
     
    ```

    ```diff
    @@ -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
       })
     }
     
    ```

    ```diff
    @@ -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
       })
     }
     
    ```

    ```diff
    @@ -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
       })
     }
     
    ```

    ```diff
    @@ -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
       })
     }
     
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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,
    ```

    ```diff
    @@ -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 {
    ```
- **#48526: fix(desktop): raise authenticated fetch timeout so profile-heavy remote startup doesn't time out (fixes #48504)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#57428`

## Fix status

- Merged to main in PR #57428 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-03`.
- Subsystem: `apps/desktop`
