Skip to content

Architecture

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

Connection Model

The app maintains independent connection paths — chat over the upstream Hermes surfaces (preferring the Dashboard Gateway, falling back to API-server SSE), 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 (preferred)WSDashboard origin (local target commonly :9119)Gateway chat via /api/ws (tui_gateway) — live thinking/reasoning
Chat (fallback)HTTP/SSEAPI Server :8642Streaming conversations via the Sessions / runs / completions APIs
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

When the dashboard gateway is available, the turn rides the /api/ws WebSocket (GatewayChatClient) and the same lifecycle events arrive over JSON-RPC, with live reasoning. The flow below is the API-server SSE fallback, used when there's no dashboard auth yet or the server is older:

  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 preferred, API-server SSE as fallback); the relay is a separate, optional surface for bridge/terminal/notifications.

AspectVanilla Hermes Chat (gateway / API fallback)Relay (Bridge/Terminal/Notifications)
ProtocolWS (/api/ws) preferred · HTTP/SSE fallbackWSS
ConnectionPersistent gateway socket · per-request on SSE fallbackPersistent
AuthDashboard ws-ticket (gateway) · API bearer token (SSE fallback)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 SSE fallback