{"record":{"id":"2193b1467e7e79cb","repo":"NousResearch/hermes-agent","slug":"local-hermes-backend-is-http-reachable-but-the-web","errorCode":null,"errorMessage":"Local Hermes backend is HTTP-reachable but the WebSocket (/api/ws) rejected the session token: ${wsProbe.reason}","messagePattern":"Local Hermes backend is HTTP-reachable but the WebSocket \\(/api/ws\\) rejected the session token: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/main.ts","lineNumber":8614,"sourceCode":"    }\n\n    const baseUrl = `http://127.0.0.1:${port}`\n    await advanceBootProgress('backend.wait', 'Waiting for Hermes backend to become ready', 90)\n    await Promise.race([waitForHermes(baseUrl, token), backendStartFailed])\n    backendReady = true\n    backendStartFailure = null\n\n    const authToken = await adoptServedDashboardToken(baseUrl, token, {\n      childAlive: () => hermesProcess.exitCode === null && !hermesProcess.killed,\n      rememberLog\n    })\n\n    // Verify the WebSocket session token before declaring backend ready.\n    const wsUrl = `ws://127.0.0.1:${port}/api/ws?token=${encodeURIComponent(authToken)}`\n    const wsProbe = await probeGatewayWebSocket(wsUrl, { WebSocketImpl: globalThis.WebSocket })\n\n    if (!wsProbe.ok) {\n      throw new Error(\n        `Local Hermes backend is HTTP-reachable but the WebSocket (/api/ws) rejected the session token: ${wsProbe.reason}`\n      )\n    }\n\n    updateBootProgress({\n      phase: 'backend.ready',\n      message: 'Hermes backend is ready. Finalizing desktop startup',\n      progress: 94,\n      running: true,\n      error: null\n    })\n\n    // A successful boot (including a soft restart that the repair-guard\n    // chose over a hard reinstall, see #74874) means any in-flight repair\n    // attempt counter has been honoured — reset it so the next genuine\n    // failure starts fresh from attempt 1 instead of inheriting the\n    // accumulated count of the resolved episode.\n    bootstrapRepairAttempt = 0","sourceCodeStart":8596,"sourceCodeEnd":8632,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/main.ts#L8596-L8632","documentation":"Local-boot path variant of the WS token verification: the locally started Hermes backend (dashboard/serve child) answered HTTP, adoptServedDashboardToken obtained the session token from the child, but the subsequent WebSocket probe to ws://127.0.0.1:<port>/api/ws?token=... was rejected. Boot aborts at ~progress-94 stage because the real app transport (WS) cannot authenticate even though REST works. It almost always indicates token mismatch between the spawned child and the probe, or an unexpected process on the port.","triggerScenarios":"probeGatewayWebSocket on the adopted dashboard token fails: token rejected during WS upgrade (401/403), the adopted token belongs to a different (stale) process on the same port, or the child died between the HTTP adoption and the WS probe.","commonSituations":"Leftover 'hermes dashboard'/'hermes serve' process from a crashed prior session occupying the port; mixed versions after an app update (runtime adopts one token scheme, desktop probes another); very fast machines/timing windows where the child rotates tokens at readiness.","solutions":["Inspect wsProbe.reason in the thrown message for 401/403 vs connection-closed","Kill orphan hermes serve/dashboard processes (pgrep -af 'hermes (serve|dashboard)') and retry boot","Reinstall/align the managed hermes runtime with the desktop app version","Check the child's captured logs (rememberLog output surfaced in boot diagnostics) for auth errors on /api/ws","Retry once after a clean disconnect; transient adoption-vs-readiness races resolve on a fresh boot"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"function isLocalWsAuthFailure(e: unknown): boolean {\n  return e instanceof Error && e.message.startsWith('Local Hermes backend is HTTP-reachable but the WebSocket')\n}","tryCatchPattern":"try { await startLocalBackend() } catch (e) { if (isLocalWsAuthFailure(e)) { await cleanupOrphanBackends(); await startLocalBackend() } else throw e }","preventionTips":["Quit the desktop app cleanly so locally spawned backends exit with it","After app updates, verify no orphan 'hermes serve' processes hold the port","Read wsProbe.reason: 401/403 points to token/version mismatch, closed points to a dying child"],"tags":["websocket","auth","local-backend","gateway","desktop","startup"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}