Two-hop connection model — socket state vs upstream health #11

Closed
opened 2026-07-29 17:32:07 +00:00 by aiko · 1 comment
Owner

What to build

Decided: the go-between pushes a dedicated upstream_status message on connect and on every change, carrying the /health result, the agent name it saw, last-checked time, and a failure reason. Not piggybacked on an existing status message, and not polled over HTTP. Client never polls; the socket is the only transport.

Every failure screen, the onboarding gate and the 局 settings tab depend on the client knowing which of two hops is broken:

client ──WebSocket──▶ go-between ──HTTP/SSE──▶ Hermes

Today "connected" means "the socket opened", which is why the current app can sit silently with a live socket and a dead upstream. WebSocketContext must carry both hops independently: local socket state, and the go-between's reported view of Hermes (/health result, agent name, last check, failure reason).

The client never talks to Hermes directly and must never hold the Hermes bearer key — upstream status is reported by the go-between and shown read-only.

Deliver the LOADING (同調中) treatment against this: tuning static with a sweeping bar, not a spinner over an empty stage.

aiko/ollvt-hermes-bridge#17 tracks the go-between side.

Design reference: docs/design/denpa-receiver/README.md (card ids 1a, 4a… index into Denpa Receiver.dc.html, openable in a browser).

Acceptance criteria

  • Message shape for upstream health agreed with the go-between and documented in the repo
  • WebSocketContext exposes both hops separately, each with a status and a one-line reason
  • Auth-refused (401) is a distinct state from unreachable, and does not auto-retry
  • 同調中 LOADING renders the tuning-static treatment
  • Client holds no Hermes bearer key
  • Tests cover each hop-state combination the failure screens key off

Blocked by


Reality audit

Governing rule: build to reality, Hermes leads. Not doing something is acceptable if it does not break core functionality. Breaking the design is acceptable. Building UI that does nothing is not.

Audited against the actual wire surface:

  • Hermes (hermes-agent 0.19.0, verified running): /health, /v1/models, /v1/runs, /v1/runs/{id}, /v1/runs/{id}/events, /v1/runs/{id}/stop, /v1/runs/{id}/approval
  • Run events: message.delta, tool.started, tool.completed, run.completed, run.cancelled, run.failed/run.errored, reasoning.available, approval.request (carries a per-request choices array and sets run status waiting_for_approval; the go-between discards it today — see #19)
  • Go-between → client: full-text, audio, control, error, set-model-and-conf, config-switched, config-files, background-files, history-list, history-data, new-history-created, history-deleted, user-input-transcription, backend-synth-complete, force-new-message, tool_call_status, interrupt-signal, heartbeat-ack, group-update
  • Client → go-between: text-input, mic-audio-data, mic-audio-end, raw-audio-data, ai-speak-signal, interrupt-signal, fetch-history-list, fetch-and-set-history, create-new-history, delete-history, fetch-configs, switch-config, fetch-backgrounds, audio-play-start, request-init-config, heartbeat

Rescoped: no upstream health message exists

There is no upstream_status message and no path by which the go-between reports Hermes'' health. The earlier decision to add one stands as a request (aiko/ollvt-hermes-bridge#17 item 1), but this issue must not ship a hop indicator that is permanently "unknown".

What is real today:

  • Socket state — open / closed / connecting, plus auth rejection. Fully knowable client-side.
  • Upstream failure, after the fact — a run that fails surfaces as an error message. HermesRunFailedError carries the run''s failure reason. So the client learns the upstream is broken when a turn fails, not before.

Build that, and nothing more. Two states, honestly labelled: the socket hop is live-monitored; the upstream hop is last known, stamped with when it was last proven, and unknown until the first turn.

Do not draw a live upstream indicator, a health poll, or a /health 200 readout. The client never talks to Hermes directly and has no data for any of them.

LOADING (同調中) is unaffected and still ships.

When the go-between grows the reporting message, the upstream hop upgrades from last-known to live with no UI change — the two-state model already accommodates it.

## What to build **Decided:** the go-between pushes a dedicated ``upstream_status`` message on connect and on every change, carrying the ``/health`` result, the agent name it saw, last-checked time, and a failure reason. Not piggybacked on an existing status message, and not polled over HTTP. Client never polls; the socket is the only transport. Every failure screen, the onboarding gate and the 局 settings tab depend on the client knowing *which of two hops* is broken: `client ──WebSocket──▶ go-between ──HTTP/SSE──▶ Hermes` Today "connected" means "the socket opened", which is why the current app can sit silently with a live socket and a dead upstream. `WebSocketContext` must carry both hops independently: local socket state, and the go-between's reported view of Hermes (`/health` result, agent name, last check, failure reason). The client never talks to Hermes directly and must never hold the Hermes bearer key — upstream status is *reported by* the go-between and shown read-only. Deliver the LOADING (同調中) treatment against this: tuning static with a sweeping bar, not a spinner over an empty stage. aiko/ollvt-hermes-bridge#17 tracks the go-between side. Design reference: `docs/design/denpa-receiver/README.md` (card ids `1a`, `4a`… index into `Denpa Receiver.dc.html`, openable in a browser). ## Acceptance criteria - [ ] Message shape for upstream health agreed with the go-between and documented in the repo - [ ] `WebSocketContext` exposes both hops separately, each with a status and a one-line reason - [ ] Auth-refused (401) is a distinct state from unreachable, and does not auto-retry - [ ] 同調中 LOADING renders the tuning-static treatment - [ ] Client holds no Hermes bearer key - [ ] Tests cover each hop-state combination the failure screens key off ## Blocked by - aiko/ollvt-hermes-bridge#17 --- ## Reality audit Governing rule: **build to reality, Hermes leads. Not doing something is acceptable if it does not break core functionality. Breaking the design is acceptable. Building UI that does nothing is not.** Audited against the actual wire surface: - **Hermes** (`hermes-agent 0.19.0`, verified running): `/health`, `/v1/models`, `/v1/runs`, `/v1/runs/{id}`, `/v1/runs/{id}/events`, `/v1/runs/{id}/stop`, `/v1/runs/{id}/approval` - **Run events**: `message.delta`, `tool.started`, `tool.completed`, `run.completed`, `run.cancelled`, `run.failed`/`run.errored`, `reasoning.available`, `approval.request` (carries a per-request `choices` array and sets run status `waiting_for_approval`; the go-between discards it today — see #19) - **Go-between → client**: `full-text`, `audio`, `control`, `error`, `set-model-and-conf`, `config-switched`, `config-files`, `background-files`, `history-list`, `history-data`, `new-history-created`, `history-deleted`, `user-input-transcription`, `backend-synth-complete`, `force-new-message`, `tool_call_status`, `interrupt-signal`, `heartbeat-ack`, `group-update` - **Client → go-between**: `text-input`, `mic-audio-data`, `mic-audio-end`, `raw-audio-data`, `ai-speak-signal`, `interrupt-signal`, `fetch-history-list`, `fetch-and-set-history`, `create-new-history`, `delete-history`, `fetch-configs`, `switch-config`, `fetch-backgrounds`, `audio-play-start`, `request-init-config`, `heartbeat` ### Rescoped: no upstream health message exists There is no `upstream_status` message and no path by which the go-between reports Hermes'' health. The earlier decision to add one stands as a *request* (aiko/ollvt-hermes-bridge#17 item 1), but this issue must not ship a hop indicator that is permanently "unknown". **What is real today:** - **Socket state** — open / closed / connecting, plus auth rejection. Fully knowable client-side. - **Upstream failure, after the fact** — a run that fails surfaces as an `error` message. `HermesRunFailedError` carries the run''s failure reason. So the client learns the upstream is broken **when a turn fails**, not before. **Build that, and nothing more.** Two states, honestly labelled: the socket hop is live-monitored; the upstream hop is *last known*, stamped with when it was last proven, and unknown until the first turn. Do **not** draw a live upstream indicator, a health poll, or a `/health 200` readout. The client never talks to Hermes directly and has no data for any of them. `LOADING` (同調中) is unaffected and still ships. When the go-between grows the reporting message, the upstream hop upgrades from last-known to live with no UI change — the two-state model already accommodates it.
Author
Owner

Closing in favour of #27, rewritten against the ADRs.

This issue's premise changed with ADR-0001: Denpa is a Hermes gateway Platform adapter and the api_server HTTP surface is abandoned entirely. Patching the body would have left a trail of corrections on top of a stale audit block - the same pattern that got 承認 wrongly cut once already.

Decisions now live in docs/adr/ in the go-between repo (branch docs/hermes-platform-adrs). The replacement issue references them rather than restating them.

Closing in favour of #27, rewritten against the ADRs. This issue's premise changed with ADR-0001: Denpa is a Hermes gateway Platform adapter and the `api_server` HTTP surface is abandoned entirely. Patching the body would have left a trail of corrections on top of a stale audit block - the same pattern that got 承認 wrongly cut once already. Decisions now live in `docs/adr/` in the go-between repo (branch `docs/hermes-platform-adrs`). The replacement issue references them rather than restating them.
aiko closed this issue 2026-07-30 00:03:48 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiko/denpa#11
No description provided.