NousResearch/hermes-agent · error · Error

Hermes backend for profile "${profile}" is HTTP-reachable bu

Error message

Hermes backend for profile "${profile}" is HTTP-reachable but the WebSocket (/api/ws) rejected the session token: ${wsProbe.reason}

What it means

During desktop boot for a given profile, the locally spawned Hermes backend answered HTTP on 127.0.0.1:<port>, but the verification WebSocket to /api/ws?token=... rejected the session token that was just adopted. Because HTTP /api/status and WS auth are separate transports with separate guards, the HTTP leg can pass while WS auth fails. The message exists to surface that split explicitly instead of letting boot fail later with a generic connect error.

Source

Thrown at apps/desktop/electron/main.ts:8274

  const baseUrl = `http://127.0.0.1:${port}`
  await Promise.race([waitForHermes(baseUrl, token), startFailed])
  ready = true

  const authToken = await adoptServedDashboardToken(baseUrl, token, {
    childAlive: () => child.exitCode === null && !child.killed,
    label: `Hermes backend for profile "${profile}"`,
    rememberLog
  })

  entry.token = authToken

  // Verify the WebSocket session token before declaring backend ready.
  // HTTP /api/status can pass while WS auth fails (separate transport, separate guards).
  const wsUrl = `ws://127.0.0.1:${port}/api/ws?token=${encodeURIComponent(authToken)}`
  const wsProbe = await probeGatewayWebSocket(wsUrl, { WebSocketImpl: globalThis.WebSocket })

  if (!wsProbe.ok) {
    throw new Error(
      `Hermes backend for profile "${profile}" is HTTP-reachable but the WebSocket (/api/ws) rejected the session token: ${wsProbe.reason}`
    )
  }

  return {
    baseUrl,
    mode: 'local',
    source: 'local',
    authMode: 'token',
    token: authToken,
    profile,
    wsUrl,
    logs: hermesLog.slice(-80),
    ...getWindowState()
  }
}

function stopPoolBackend(profile) {

View on GitHub (pinned to c896c09c42)

Solutions

  1. Read wsProbe.reason in the message to see if it is an HTTP 401/403 during upgrade (auth) vs timeout/closed (process)
  2. Kill stale Hermes backend processes for that profile and retry the boot so a fresh process owns the port
  3. Update the desktop app and the hermes runtime to matching versions so token minting and WS auth agree
  4. Check gateway logs (~/.hermes/logs/gateway.log for the profile) for WS handshake rejections at boot time
  5. Retry the connection once — a token adopted during process warmup can fail transiently on first probe
Defensive patterns

Strategy: retry

Type guard

function isProfileWsAuthFailure(e: unknown, profile: string): boolean {
  return e instanceof Error && e.message.includes(`profile "${profile}" is HTTP-reachable but the WebSocket`)
}

Try / catch

try { await bootProfileBackend(profile) } catch (e) { if (isProfileWsAuthFailure(e, profile)) { await killStaleHermesBackends(profile); await bootProfileBackend(profile) } else throw e }

Prevention

When it happens

Trigger: Profile backend spawn path: HTTP status check passes, adoptServedDashboardToken/authToken is obtained, then probeGatewayWebSocket(wsUrl) returns !ok — e.g. the gateway rejected the token (401/403 on upgrade), the token was minted for a different session/expired between HTTP check and WS probe, or a stale process is squatting on the port.

Common situations: A previous Hermes backend instance still holds the port so the new spawn's token is checked against the old process; gateway version mismatch where the token format or WS auth changed; slow machines where the token TTL lapses during boot; gateway auth middleware rejecting query-param tokens in favor of tickets.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/86437db3ea646309. Report an issue: GitHub.