Files
hermes-workspace/docs/workspace-chat-session-routing.md
Eric 4f75b5835c fix: batch remaining workspace bugfix slices (#483)
* chore: preserve pnpm approved builds

* fix(router): support runtime basepath override for reverse-proxy hosting

The TanStack router was created without a `basepath` option, so the same
built bundle could not be hosted under a path prefix (e.g. behind a
reverse proxy that mounts the app at `/workspaces/<id>/`). Hard refreshes
appeared to work because SSR runs at the proxy-stripped path, but
client-side navigation to dynamic routes such as `/chat/$sessionKey`
silently fell through to the catch-all `/$` route — rendering the
"404 — Not Found" page from inside the SPA.

Read an optional `window.__HERMES_WORKSPACE_BASEPATH__` global and pass
it through to `createRouter`. When unset, behavior is unchanged
(`basepath: '/'`). The value is normalized so callers can pass either
`/workspaces/abc`, `workspaces/abc`, or `/workspaces/abc/` without
upsetting TanStack's pathname matching.

This lets hosting layers inject a tiny inline script before the bundle
loads to mount the app at any path, without rebuilding.

* fix(chat): prevent stale thinking state after page refresh (closes #449)

Root cause: sessionStorage 'waiting' flags persisted across page refreshes
even for completed conversations. The Zustand store restored these stale
entries on mount, and the active-run API check cleared them async —
but there was a visible render window where the UI showed 'thinking'.

Fix:
1. Added activeRunCheckDone state that gates the waitingForResponse memo.
   While the active-run API check is pending, stale restored state is
   not trusted — the thinking indicator stays hidden until verification.
2. Added onCheckComplete callback to useActiveRunCheck hook that fires
   after the API check finishes (success or error), unblocking the gate.
3. Added a useEffect that detects restored stale waiting state and sets
   pendingVerifySessionKeyRef so the gate only applies to the key that
   needs verification — not to genuine active streams.

Test: e2e/chat-thinking-state.spec.ts injects a stale sessionStorage
entry before page load, then verifies no thinking indicator appears
and the stale entry is cleaned up by the API check.

* fix(chat): eliminate duplicate messages flicker on stream completion (closes #441)

Root cause: onDone handler used queryClient.invalidateQueries() which
triggers an async refetch. During the refetch window, mergeHistoryMessages
ran with stale cache data + realtime buffer, producing visible duplicates
(extra user message + blank line) for 1-2 seconds until refetch completed.

Fix: Directly merge realtime buffer into history cache via setQueryData(),
then clear buffer synchronously. Background refetch runs after for
consistency but doesn't block rendering.

* fix: restore hermes-config and config-patch API routes

The Aurora rename migration (efcb7d14) renamed hermes-config.ts to
claude-config.ts, but the frontend and routeTree.gen.ts still reference
the original paths. This caused all /api/hermes-config and /api/config-patch
requests to fall through to the SPA HTML fallback, breaking config saves
from the settings dialog and provider wizard with 'Failed to save' errors.

Restored by creating thin route files that delegate to the existing
handleHermesConfigGet/handleHermesConfigPatch handlers from
src/server/hermes-config-route.ts.

Fixes the settings dialog (hermes-config GET/PATCH) and provider wizard
(config-patch POST) config save flows.

* fix(server): add essential env vars to terminal session

* fix(swarm): worker card shows stale state after task completes

deriveWorkerState derived the badge from currentTask title substring
matching and markCheckpointResult never cleared currentTask on terminal
checkpoints, so a finished worker's card stayed permanently 'working'.

- swarm-dispatch.ts: clear currentTask on terminal checkpoint
  (checkpointStatus !== 'in_progress'), matching conductor-stop's reset
- operational-worker-card.tsx: deriveWorkerState reads authoritative
  checkpointStatus/state first, title heuristic only while in_progress
- swarm2-screen.tsx: pass checkpointStatus/state into the card

* fix(portable-history): replay authenticated portable chat history

* fix(config): keep legacy claude-config shim on shared handlers

* fix: harden splash hydration and docker uid mapping

* fix: keep seen update notes dismissed

* feat: consolidate workspace state under configurable state directory (closes #439)

Adds HERMES_WORKSPACE_STATE_DIR env var support, consolidating 5
scattered state files under a single configurable directory.

Changes:
- New src/server/workspace-state-dir.ts with getStateDir() utility
  honoring HERMES_WORKSPACE_STATE_DIR → HERMES_HOME/workspace →
  CLAUDE_HOME/workspace → ~/.hermes/workspace (fallback chain)
- Updated gateway-capabilities.ts (workspace-overrides.json)
- Updated mcp-presets-store.ts (mcp-presets.json)
- Updated mcp-hub-sources-store.ts (mcp-hub-sources.json)
- Updated mcp-tools-cache.ts (cache/mcp-tools.json)
- Updated knowledge-config.ts (knowledge-config.json)
- Removed 5 duplicated hermesHome() functions, replaced with shared
  getStateDir() import

Test: 6 vitest unit tests covering all env var priority combinations
(cherry picked from commit d6bebe0614b0c7b9015bac5e35d315a8450ac146)

* fix(conductor): surface native-swarm progress and harden worker startup

* feat(chat): safely render HTML message markup

* fix(chat): surface installed skills in slash autocomplete

* fix: add swarm runtime reset endpoint

* fix(conductor): mobile rendering — add overflow-y-auto, mobile bottom padding, OfficeView responsive height, tabbar fix

* fix(send-stream): preserve runs on client disconnect

* fix(profiles): skip profiles/default duplicate card

* fix: accept HERMES_AGENT_PATH override

* fix(profiles): allow disabling sticky active_profile writes

* fix: preserve workspace chat session routing

* fix(portable-history): skip replay when gateway session continuity is available

---------

Co-authored-by: Hermes Agent <hermes-agent@local.invalid>
Co-authored-by: jack <jack@hijak.dev>
Co-authored-by: Waylon Kenning <waylonkenning@Waylons-MacBook-Pro.local>
Co-authored-by: Michael Rodriguez <michael@rivercity-industries.com>
Co-authored-by: Vu Tran <baysao@gmail.com>
Co-authored-by: iltaek <iltaekkwon@gmail.com>
Co-authored-by: Aurora release bot <release@outsourc-e.com>
Co-authored-by: jonathanmalkin <jonathan.d.malkin@gmail.com>
Co-authored-by: KT-Hermes <ktadmin@kt-bot2.tekeis.net>
2026-05-19 16:27:10 -04:00

4.2 KiB

Workspace Chat Session Routing

Purpose

Hermes Workspace supports a portable chat path through OpenAI-compatible /v1/chat/completions. In this mode, the browser route alone is not enough to preserve conversational context: Workspace must forward a stable server-side session identifier to the Hermes Agent gateway.

This document records the routing contract and the failure mode that caused related turns and attachments to be stored as separate api-* sessions.

Routing Contract

There are two distinct header layers:

Layer Headers Purpose
Workspace UI route resolution X-Hermes-Session-Key, X-Hermes-Friendly-Id Tells the browser which Workspace chat route/friendly ID is resolved for the visible conversation.
Hermes Agent gateway continuation X-Hermes-Session-Id, X-Claude-Session-Id Tells the gateway which server-side Hermes session should receive the next chat completion request.

Do not conflate these. A response can correctly resolve a Workspace route while the next gateway request still loses server-side context if X-Hermes-Session-Id is missing.

Portable OpenAI-Compatible Flow

  1. src/routes/api/send-stream.ts receives sessionKey, friendlyId, message, history, and optional attachments from the UI.
  2. It resolves a persistent Workspace sessionKey.
  3. It builds OpenAI-compatible messages, including multimodal image parts when attachments are present.
  4. It calls openaiChat(..., { sessionId: portableSessionKey }).
  5. src/server/openai-compat-api.ts forwards that session ID to the gateway via:
    • X-Hermes-Session-Id
    • X-Claude-Session-Id as a legacy/back-compat alias.
  6. Hermes Agent uses the provided session ID for continuity instead of deriving a fresh deterministic api-* session from the request payload.

Failure Mode

The bug was coupling session-continuity headers to bearer-token presence:

if (options.sessionId && bearer) {
  headers['X-Hermes-Session-Id'] = options.sessionId
  headers['X-Claude-Session-Id'] = options.sessionId
}

That made routing depend on auth configuration. If a bearer token was unavailable or not used, Workspace still had a local session key, but the gateway never received it. The gateway then derived sessions such as api-* from request content, which could split related turns and attachment-only/image requests across separate API sessions.

Correct Behavior

Session routing is independent of whether a bearer token is configured. If the gateway requires auth, its auth check enforces the bearer token separately.

const bearer = getBearerToken()
if (bearer) {
  headers['Authorization'] = `Bearer ${bearer}`
}

if (options.sessionId) {
  headers['X-Hermes-Session-Id'] = options.sessionId
  headers['X-Claude-Session-Id'] = options.sessionId
}

Regression Coverage

src/server/openai-compat-api.test.ts should cover both cases:

  • session headers are sent when a bearer token is present
  • session headers are still sent when no bearer token is present

src/server/chat-backends.ts should forward options.sessionId into openaiChat(...) for both streaming and non-streaming OpenAI-compatible calls.

Manual Verification Recipe

  1. Run the targeted test:

    pnpm vitest run src/server/openai-compat-api.test.ts
    
  2. Build production assets:

    pnpm build
    
  3. Restart Workspace where deployed:

    systemctl --user restart hermes-workspace.service
    systemctl --user is-active hermes-workspace.service
    
  4. Send two /api/send-stream turns with the same sessionKey and a unique token in the first prompt.

  5. Search session history for that token. Both turns should appear under the same session_id equal to the supplied Workspace session key, not separate api-* sessions.

  6. Send an image attachment with the same sessionKey; session history should show [screenshot] in that same session.

Operational Notes

  • Keep credentials redacted when inspecting .env, service files, or built bundles.
  • In zero-fork deployments, Workspace commonly talks to Hermes Agent gateway on 127.0.0.1:8642 and Dashboard on 127.0.0.1:9119.
  • A successful /health probe means the gateway is reachable; it does not prove session continuity is wired correctly. Verify the actual chat path.