Skip to content

Architecture

Select the diagram to inspect it at full size.
How Hermes-Relay connects: upstream Hermes owns standard chat, Manage, voice and inbound files; the encouraged Relay extension fills current gaps for terminal, notifications, desktop tools, enhanced voice and Relay sessions; Device Control also needs the sideload build.

Connection Model

The app maintains independent connection paths — standard chat over the upstream Dashboard Gateway, explicit API-only chat over Direct API, and persistent WSS for Relay extensions. Relay is optional for the upstream standard path but encouraged for the full current feature set; compatible upstream surfaces take precedence as they ship.

For a compact shareable reference covering connection paths, transport boundaries, pairing/session lifecycle, and operator controls, see the Relay Architecture Spec.

PathProtocolServerPurpose
Chat (standard)WSDashboard origin (local target commonly :9119)Gateway chat via /api/ws (tui_gateway) — live thinking/reasoning
Chat (Direct API)HTTP/SSEAPI Server :8642API-only compatibility conversations via Sessions / runs / completions
TerminalWS/WSSSelected Dashboard origin · same-origin Relay ingressRemote shell via tmux (Phase 2)
BridgeWS/WSSSelected Dashboard origin · same-origin Relay ingressDevice control via AccessibilityService + MediaProjection (Phase 3)
NotificationsWS/WSSSelected Dashboard origin · same-origin Relay ingressNotificationListenerService forwards posted notifications over a bounded channel

The Relay process still owns one internal listener on :8767, but normal LAN, Tailscale, and public clients reach it through the Dashboard plugin path on the Dashboard origin. Direct external :8767 is retained only for explicit legacy compatibility. The older standalone android_relay.py service on port 8766 is retired. For recommended Tailscale, the selected origin uses the helper-reported dedicated HTTPS listener (:10443 by default) and proxies the local Dashboard target on :9119. The dedicated port avoids colliding with an existing Traefik, Caddy, or nginx listener on :443.

Key Components

ComponentPurpose
HermesApiClientDirect HTTP/SSE client for Hermes API Server
ChatHandlerMessage state management and streaming event processing
ChatViewModelSession CRUD, message sending, personality selection
ConnectionViewModelDual connection model, API client lifecycle, settings
ConnectionManagerWebSocket connection for relay (bridge/terminal)
ChannelMultiplexerEnvelope routing for relay channels
AuthManagerAPI key and session token storage (encrypted)
ConnectivityObserverNetwork connectivity monitoring

Chat Message Flow

Standard chats ride the /api/ws WebSocket (GatewayChatClient) and receive lifecycle events over JSON-RPC with live reasoning. The flow below is the explicit Direct API path for API-only/headless compatibility connections:

  1. User types a message in ChatScreen
  2. ChatViewModel creates a session (if needed) via POST /api/sessions
  3. Message sent via POST /api/sessions/{id}/chat/stream
  4. HermesApiClient receives SSE events on OkHttp thread pool
  5. Events dispatched to main thread via Handler
  6. ChatHandler updates StateFlows (messages, streaming, tools)
  7. Compose UI recomposes from StateFlow changes

SSE Event Pipeline

The Hermes API Server streams events using Server-Sent Events. Each event type maps to a specific UI update.

EventHandler Action
session.createdInitialize session context (session_id, run_id, title)
run.startedRecord run start, capture user_message object
message.startedCreate assistant message placeholder from message object (id, role)
assistant.deltaAppend text delta to streaming message
tool.progressAppend reasoning/thinking delta to message
tool.pendingCreate tool progress card (queued state)
tool.startedUpdate card with start time, preview, args
tool.completedMark card as done with result_preview
tool.failedMark card as failed with error
assistant.completedFinalize message (content, completed, partial, interrupted flags)
run.completedEnd streaming state (completed, partial, interrupted, api_calls)
errorDisplay error banner (message, error)
doneClose SSE connection (state: "final")

Relay Auth Flow

The relay connection (bridge/terminal) uses a pairing code for initial setup, then session tokens for persistence.

Pairing codes use the full A-Z / 0-9 alphabet (36 chars). The pair command (hermes pair, /hermes-relay-pair, or the compatibility hermes-pair shell shim) on the Hermes host mints the code and pre-registers it with the relay via a loopback-only /pairing/register endpoint before embedding it in the QR — so the phone never types a code by hand. Session tokens are stored in EncryptedSharedPreferences backed by Android Keystore.

Vanilla Hermes chat vs Relay

Chat uses vanilla upstream Hermes either way (Gateway for standard connections, Direct API for API-only compatibility); the relay is a separate, optional surface for bridge/terminal/notifications.

AspectVanilla Hermes Chat (Gateway / Direct API)Relay (Bridge/Terminal/Notifications)
ProtocolWS (/api/ws) for Gateway · HTTP/SSE for Direct APIWSS
ConnectionPersistent Gateway socket · per-request on Direct APIPersistent
AuthDashboard ws-ticket (Gateway) · API bearer token (Direct API)Pairing code + session token. Voice endpoints may also accept the API bearer token.
ServerHermes Dashboard origin · Hermes API :8642Dashboard origin → local :9119 → internal Relay :8767
Live reasoningYes on Gateway · post-hoc only on Direct API