hive-claude 0.1.4 (hive-claude #28) sends the idle watchdog's signal as SIGINT to claude's process group instead of SIGKILL. It then waits for claude to exit, with no SIGKILL fallback. claude stops the commands its Bash tool started when it shuts down on SIGINT, and a SIGKILL left them running. This is the last SIGKILL of a claude process from #4999. hive-agent's `idle_timeout` -> `Error::IdleTimeout` -> `TurnError::ApiStall` path is unchanged. The difference is that the error now arrives only once claude has exited. A claude that ignores the SIGINT keeps the turn open until it is killed by hand or the unit stops. The Cargo.lock change is only hive-claude's version and checksum. A plain `cargo update -p hive-claude --precise 0.1.4` also moved seven crates' Windows-only `windows-sys` edges from 0.61.2 to 0.52.0, unrelated to this bump. The minimal lock passes `--locked`. The turn-loop and web-UI docs, and hive-agent's watchdog docs and stall note, now say SIGINT instead of "killed". That includes the ACP spots that the previous commit's `Drop` change made stale.
453 lines
25 KiB
Markdown
453 lines
25 KiB
Markdown
# Per-agent page
|
||
|
||
> Part of [Web UI](README.md). See also:
|
||
> [Shape (shared)](shape.md) · [Dashboard layout](dashboard.md)
|
||
|
||
|
||
Three fixed-position layers frame a full-viewport terminal: a header,
|
||
scrollable main content, and a footer composer — plus a slide-in side
|
||
panel for flyouts and long content.
|
||
|
||
## Header
|
||
|
||
Preact component tree (`Header.tsx` + `StatusChips.tsx` +
|
||
`MetaNav.tsx` + `HeaderPill.tsx`, wired together in `Root.tsx`) — see
|
||
`frontend/packages/agent/src/components/`. This section describes the
|
||
rendered component tree: every element is Preact component output.
|
||
|
||
**Fixed-overlay header** (`<header class="agent-header">`): frosted
|
||
glass — `backdrop-filter: blur` lets scrolled terminal rows show
|
||
through. Measures its own rendered height via `ResizeObserver`
|
||
(`Header.tsx`) and writes it to a CSS custom property the content
|
||
below reads for its offset, so content never overlaps the header
|
||
regardless of how many rows of badges/pills wrap onto an extra line
|
||
at a given viewport width. Two columns:
|
||
|
||
<!-- vale write-good.Passive = NO -->
|
||
- **Agent icon** (`<img class="agent-icon">`): fixed-size square
|
||
identity anchor — `width: 5em; height: 5em` with explicit pixel
|
||
sizing so the `<img>`'s intrinsic (large) dimensions don't push
|
||
the parent flex container open via `align-items: stretch`-driven
|
||
height feedback. Falls back to the dimmed hyperhive mark
|
||
(`/favicon.svg`) on load error (`/icon` 404s when the agent has no
|
||
`services.hyperhive.agent.icon` override — there is no server-side default image).
|
||
- **Main column** (`.agent-header-main`, `Header.tsx`): two rows —
|
||
title (`◆ <label> ◆`) and, when set, the "swarm / hive" identity
|
||
line. Nothing else lives here; both rows are short, fixed-shape text
|
||
that doesn't wrap in practice, so this column's height stays stable
|
||
regardless of how many badges or pills are showing (a design
|
||
correction mid-review — the first pass put the variable-width
|
||
content here instead, which is exactly the kind of thing the
|
||
`ResizeObserver` measurement above exists to catch even when a
|
||
layout choice reintroduces it).
|
||
- **Pills cluster** (`.agent-header-pills`, right-aligned): status
|
||
badges (`StatusChips.tsx`) and flyout triggers together in one row,
|
||
so the header reads as one identity zone + one status/actions zone:
|
||
- **Alive badge**: `● alive` (green) / `⊘ rate limited` (red) /
|
||
`◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
|
||
- **State badge**: `💤 idle` / `🧠 thinking` / `📦 compacting` /
|
||
`○ offline` / `… booting` + age suffix.
|
||
- **Model badge** (`model · <name> ▾`): a real picker — selecting it opens
|
||
a `Dropdown` of `state.available_models`, selecting one POSTs
|
||
`/api/model` immediately (same endpoint the `/model <name>` slash
|
||
command uses). A header badge: the control and its display sit
|
||
together.
|
||
- **Effort badge** (`effort · <level> ▾`): same shape, `/api/effort`,
|
||
shown when `state.available_efforts` is non-empty.
|
||
- On an ACP agent both pickers list what its session offers (its
|
||
`model` and `thought_level` config options), and are empty until the
|
||
first turn has attached a session. The effort picker is hidden when the
|
||
model offers no effort levels, and while a newly picked model waits for
|
||
the next turn, since the levels come with the model.
|
||
- **Ctx / cost badges**: `ctx · 142k` (last inference's prompt size,
|
||
tooltip shows % of context window) / `cost · 1.3M` (cumulative
|
||
tokens billed across every inference in the last turn).
|
||
- **Pause badge** (`⏸ pause` / `▶ resume`): toggles via the same
|
||
hive-c0re-owned `/api/pause/<name>` / `/api/resume/<name>`
|
||
endpoints the dashboard uses (pausing needs a write this
|
||
unprivileged process can't make directly).
|
||
- **Inbox / todos pills** (`📬 inbox · N` / `📋 todos · N`): hidden
|
||
when empty; selecting it opens the matching flyout in the side panel.
|
||
- **Links badge** (`🔗`): opens a popover listing `↑ dashboard`
|
||
(back-link to the host dashboard, `${dashboardBase}dashboard.html`)
|
||
followed by this agent's `agent_links()`-sourced navigation (stats,
|
||
screen when VNC is enabled, forge profile + config-repo mirror when
|
||
the agent has a forge account, any `services.hyperhive.agent.dashboardLinks`
|
||
extras) — all as real `<a>` elements, not a `Dropdown`-style
|
||
command list, so ctrl/middle-click and "copy link address" keep
|
||
working. Each `AgentLink.kind` resolves differently: `container` →
|
||
same-origin path; `forge` → `state.forge_public_url + url`, and
|
||
hive-agent drops the link entirely when that's unset (never guessed from
|
||
`<host>:3000`); `external` → already absolute. Same source
|
||
(`GET /api/state`'s `links` field) also feeds
|
||
`DashboardState.links` for the dashboard card's icon strip —
|
||
`agent_links()` in hive-agent is the single source of truth for
|
||
both. The dashboard link lives in this 🔗 popover; the model/effort
|
||
pickers are real badges above; and `/new-session` / `/logout` are
|
||
typed slash commands with their own type-twice confirm (see
|
||
below).
|
||
- `/cancel` (slash command, below) is the only cancel-turn path; the
|
||
turn-loop state badge already shows `thinking` as the
|
||
discoverability cue.
|
||
<!-- vale write-good.Passive = YES -->
|
||
|
||
Values throughout come from `GET /api/state`'s cold-load snapshot,
|
||
kept in sync afterwards by the SSE stream (see Live view below) —
|
||
`context_window_tokens` for the ctx badge tooltip, `qualified_label`
|
||
(the hive-qualified `name@domain` form, used for the browser tab title
|
||
so two tabs from different hives are distinguishable; the header's own
|
||
`◆ <label> ◆` stays short).
|
||
|
||
## Main content
|
||
|
||
**Main content** (`<main class="agent-main">`): fills the viewport
|
||
and scrolls behind the fixed header + footer.
|
||
|
||
<!-- vale write-good.Passive = NO -->
|
||
- `#status` overlay: empty when online; shows the login form / OAuth
|
||
URL when `status` is `needs_login_*`. The OAuth code input is
|
||
`type="password"` with a `👁 reveal` toggle that flips it back to
|
||
`text` on press so the operator can sanity-check the paste before
|
||
submit — avoids accidental on-screen token exposure to
|
||
shoulder-surfers or screenshots. `autocomplete="one-time-code"`
|
||
is the semantic value for OAuth codes (per WHATWG): browsers may
|
||
silently ignore `autocomplete="off"` on `type="password"`, but
|
||
browsers honour `one-time-code` and suppress the "save password
|
||
for this site?" prompt that would otherwise fire on submit.
|
||
- Terminal-wrap: live event tail (sticky-bottom autoscroll +
|
||
`↓ N new` pill when not at bottom). The pill is **anchored in
|
||
`.agent-main`**, not in `log.parentElement = .terminal-wrap`:
|
||
`.terminal-wrap` applies `backdrop-filter: blur` for the frost
|
||
effect, which creates a CSS stacking context — anchoring the
|
||
pill inside that context would trap its `z-index` below the
|
||
fixed composer in the root stacking context, and it'd never
|
||
float. `.agent-main` has no backdrop-filter (no stacking-context
|
||
creators), so the pill's `z-index` reaches the root and properly
|
||
composites above the composer. Geometry is unchanged —
|
||
`.agent-main` and `.terminal-wrap` both `inset: 0` fill the same
|
||
area.
|
||
<!-- vale write-good.Passive = YES -->
|
||
|
||
## Footer / composer
|
||
|
||
**Fixed-overlay footer** (`<footer class="agent-composer">`): frosted
|
||
glass, symmetric with the header. Contains the operator-input
|
||
textarea (`#term-input`) — multi-line, Enter sends, Shift+Enter
|
||
newlines, Tab-completes slash commands (see [Terminal-embedded
|
||
prompt](#terminal-embedded-prompt) below).
|
||
|
||
## Side panel
|
||
|
||
**Side panel** (slide-in from right): singleton shared with the
|
||
dashboard's side panel shape. Carries inbox and todos flyouts (opened
|
||
via the header pills) as well as long content (file previews, diffs,
|
||
journald logs). Inbox flyout: unread messages addressed to this agent
|
||
(`acked_at IS NULL`, newest-first, up to 30); reply messages indented
|
||
with `↳ reply ·` in amber. A `✓ mark all read` button appears in the
|
||
flyout header when the inbox is non-empty; selecting it confirms then
|
||
POSTs cross-origin to the core dashboard's `POST
|
||
/api/agent/{name}/mark-all-read`, which acks all pending messages for
|
||
this agent so the harness won't receive wake-prompts for them. A `{
|
||
marked: N }` pill surfaces the count. After the drain the inbox list
|
||
empties on reload (the filter is `acked_at IS NULL`, so drained
|
||
messages disappear). Todos flyout ("loose-ends v2"): the harness-local
|
||
todos other subsystems push at this agent (`GET /api/todos`) — matrix,
|
||
forge, and bash are the built-in producers, but any user-configured MCP
|
||
server can push its own via the same in-agent socket. Each row shows the
|
||
producing subsystem, an
|
||
optional source label, a summary, and age. A checkbox per row plus a
|
||
select-all / select-none / `✓ mark done` bulk row above the list POSTs
|
||
the checked ids to `POST /api/todos/mark-done`, which dismisses them
|
||
from the harness-local store (same effect as `cancel_loose_end(kind:
|
||
"todo")`, just from the web UI instead of the agent's own tool calls).
|
||
If the todo store errors or doesn't answer for any id, the endpoint returns
|
||
500 naming how many ids failed, and the panel shows `failed: …` instead of
|
||
`✓ marked done`.
|
||
|
||
## Live view
|
||
|
||
Each agent runs an `events::Bus`: a `tokio::sync::broadcast<LiveEvent>`
|
||
plus a sqlite-backed history at `/state/hyperhive-events.sqlite`.
|
||
The harness emits `TurnStart { from, body, unread }`,
|
||
`Stream(value)` (one per parsed stream-json line), `Note`,
|
||
`TurnEnd { ok, note }`. Each event also carries a `ts` (unix
|
||
seconds) — a flattened sibling of the event tag on both the live
|
||
SSE frame and the replayed history rows. The web UI:
|
||
|
||
- fetches `GET /events/history` on page load and replays the last
|
||
2000 events (oldest first);
|
||
- then subscribes to `GET /events/stream` (SSE) for live tail;
|
||
- shows a granular state badge above the terminal, driven
|
||
authoritatively from `/api/state.turn_state`. SSE turn_start /
|
||
turn_end still flip the badge instantly between renders;
|
||
- terminal-themed: phosphor mauve glow, Crust bg,
|
||
backdrop-filter blur, row fade-in slide-up.
|
||
|
||
The backfill/live-tail dedupe, sticky-bottom autoscroll, and "↓ N
|
||
new" pill are the shared terminal-pane mechanics described in
|
||
[Shape](shape.md#shared-terminal-pane) — this page's log is one
|
||
instance of that same factory.
|
||
|
||
Per-stream rendering (see [`docs/web-ui/terminal-rendering.md`](terminal-rendering.md) for
|
||
the full row taxonomy and dispatch logic):
|
||
|
||
- `Stream` `tool_use` →
|
||
- `Write` / `Edit`: collapsed `<details>` with a +/- diff body
|
||
(`-` lines from `input.old_string`, `+` lines from
|
||
`input.new_string` or every line of `input.content`).
|
||
Summary carries the path + line counts.
|
||
- others (`Read /path`, `Bash $ cmd`, `mcp__hyperhive__send →
|
||
operator: "..."`, etc.): flat one-line per-tool format with
|
||
per-tool salient-arg extraction (`fmtToolUse`).
|
||
- `Stream` `tool_result` short → flat `← ...`; long → collapsed
|
||
`<details>` `▸ ← Nl · headline` (select to expand full body).
|
||
- `Stream` `thinking` → `.thinking` row with a `💭 thinking …`
|
||
indicator.
|
||
- `Stream` `system` → handled by subtype: `plugin_install` and
|
||
`compact_boundary` emit muted notes; `commands_changed` emits an
|
||
expandable details row listing slash commands; `thinking_tokens`
|
||
updates a single in-place `🧠` counter; `init`, `result`, and
|
||
`rate_limit_event` drop silently (noise / used elsewhere); other
|
||
subtypes → muted `⚙ <subtype>` note.
|
||
- `Note` → `· text`.
|
||
- `TurnStart` → `◆ TURN ← <from>` with the wake-prompt body; a
|
||
muted `· HH:MM:SS` time suffix from the event `ts`.
|
||
- `TurnEnd` → `✓ turn ok` / `✗ turn fail — note`, with a
|
||
`· HH:MM:SS · <duration>` suffix (duration = end − the paired
|
||
turn-start), triggers a `refreshState()`. The time suffix is
|
||
guarded on a numeric `ts`, so rows degrade cleanly when absent.
|
||
|
||
## Terminal-embedded prompt
|
||
|
||
The operator input lives *inside* the terminal-wrap as a
|
||
prompt-style textarea below the live tail: multi-line (Enter
|
||
sends, Shift+Enter newlines), tab-completes slash commands.
|
||
|
||
Slash commands today:
|
||
|
||
- `/help` — list commands locally.
|
||
- `/clear` — wipe the local terminal view (server history kept).
|
||
- `/cancel` — `POST /api/cancel` → SIGINT to the turn's claude and every
|
||
command it started (its process group), emits a Note. On an ACP agent
|
||
it sends the agent `session/cancel` instead, and SIGINT if the turn
|
||
hasn't ended 10s later. Also surfaces as a `■ cancel turn` button in
|
||
the state row while state=thinking.
|
||
- `/compact` — `POST /api/compact` → sets the deferred
|
||
`Bus::request_compact()` flag and returns immediately. The harness
|
||
runs the `/compact` at the next turn boundary (end of the in-flight
|
||
turn, or `turn::run_pending_compact` when idle); output streams into
|
||
the live panel. Works mid-turn, not only when idle.
|
||
- `/model <name>` — `POST /api/model` flipping `Bus::set_model`.
|
||
Takes effect on the next turn; persisted to
|
||
`/state/hyperhive-model` so the override survives harness
|
||
restart / rebuild.
|
||
- `/effort <level>` — `POST /api/effort` setting the claude effort
|
||
level (`low` / `medium` / `high` / `xhigh` / `max`). Takes effect on the next
|
||
turn. `StatusChips`' effort badge calls the same endpoint; both stay
|
||
in sync via `StateSnapshot.effort`.
|
||
- `/new-session` — `POST /api/new-session` (confirms first).
|
||
Arms a one-shot on the Bus; next turn runs without
|
||
`--continue`, dropping the resume session entirely.
|
||
- `/logout` — `POST /api/logout` (confirms first). Wipes OAuth
|
||
credential files, parks the agent in `needs_login`, and preserves
|
||
session history (`~/.claude/projects/`).
|
||
|
||
Unknown `/foo` shows an error row instead of being silently sent.
|
||
|
||
## Per-agent endpoints
|
||
|
||
Successful POSTs return 200 (no 303 redirects). Error responses
|
||
use semantic status codes: **400** for missing/invalid input
|
||
(`body` required, unknown model name, invalid effort level),
|
||
**409** for retryable state conflicts (turn in flight when
|
||
`/compact` fires), **500** only for genuine server/transport
|
||
failures. The matching
|
||
mutations fire `LiveEvent` variants on the per-agent bus, so the
|
||
client doesn't refetch `/api/state` on submit — the SSE stream
|
||
delivers the new state faster anyway. Only the login flow still
|
||
polls (session output streams in updates that aren't event-
|
||
shaped).
|
||
|
||
- `POST /send` — operator-injected message into this agent's inbox.
|
||
- `POST /login/{start,code,cancel}` — claude OAuth login flow.
|
||
Start/cancel emit `LiveEvent::StatusChanged` to flip the
|
||
badge to/from `needs_login_in_progress`.
|
||
- `POST /api/cancel` — SIGINT the in-flight claude turn and the
|
||
commands it started. Emits a `LiveEvent::Note`.
|
||
- `POST /api/compact` — run `/compact` on the persistent session
|
||
(same MCP config + system prompt + allowed tools as a normal
|
||
turn — only the stdin payload differs). Flips state to
|
||
`Compacting` via `Bus::set_state`, which emits
|
||
`TurnStateChanged`.
|
||
- `POST /api/model` (`model=<name>`) — switch the model for
|
||
future turns. `Bus::set_model` emits `ModelChanged`.
|
||
- `POST /api/effort` (`effort=<level>`) — switch the claude
|
||
effort level for future sessions. Validated server-side against
|
||
`EFFORT_LEVELS` (`low`/`medium`/`high`/`xhigh`/`max`) — unknown values are
|
||
rejected rather than forwarded to `claude --effort`. Persists via
|
||
`Bus::set_effort`, which emits `EffortChanged`. Applies on the next
|
||
session start (no mid-session swap).
|
||
- On an ACP agent, `/api/model` and `/api/effort` accept only values the
|
||
session offers, and the runtime sets them with `session/set_config_option`
|
||
before the next prompt.
|
||
- `POST /api/new-session` — arm a one-shot for the next turn to
|
||
drop `--continue`. Emits a `LiveEvent::Note`.
|
||
- `POST /api/logout` — three-step teardown that re-uses the
|
||
existing `wait_for_login` resumption path:
|
||
1. SIGINT any running claude (matches `/api/cancel`'s pattern —
|
||
idempotent no-op when nothing is running) so the credential
|
||
wipe doesn't race a mid-API-call turn.
|
||
2. Delete only the OAuth credential files
|
||
(`.credentials.json` + `mcp-needs-auth-cache.json` under
|
||
`~/.claude/`). **Preserves** session history files
|
||
(`projects/<hash>/*.jsonl`), sessions, shell-snapshots,
|
||
plans, settings, telemetry, and the dir itself, so
|
||
`claude --continue` keeps working after a fresh login.
|
||
⚠️ Deleting the whole `~/.claude/` directory instead breaks
|
||
session continuity — logout must stay narrowed to just the
|
||
credential files.
|
||
3. Flip `LoginState::NeedsLogin` + emit a `LiveEvent::Note`
|
||
naming the files it just deleted, then emit
|
||
`needs_login_idle`. The turn-loop's next iteration parks
|
||
into `wait_for_login`, which snapshots the credential dir
|
||
(now missing the wiped files) and resumes when a fresh
|
||
credentials file appears via the dashboard's `/login/code`
|
||
flow (the same mtime-resumption path manual re-login uses).
|
||
|
||
Always returns 200 with a body describing what happened — it
|
||
folds per-file errors into the response + the Note so the
|
||
operator sees them in the live panel rather than as an HTTP
|
||
error. Missing files (already logged out) count as
|
||
idempotent.
|
||
- `GET /api/state` — cold-load snapshot (`StateSnapshot`) consumed by
|
||
the frontend (`useAgentState.ts`) on page load and, on a poll, while
|
||
`status === 'needs_login_in_progress'`. Includes `turn_state`,
|
||
`context_window_tokens`, `qualified_label`,
|
||
`available_models`, `effort`, `available_efforts`, `links`, and
|
||
other fields described inline throughout this document. All
|
||
subsequent state updates arrive via SSE.
|
||
- `GET /api/dashboard-state` — lean snapshot of agent-owned fields
|
||
fetched once per running agent by the dashboard's container row to
|
||
get fresh values without relying on hive-c0re's periodic file-reads.
|
||
Returns `{ status_text?, status_set_at?, ctx_tokens?, context_window_tokens,
|
||
rate_limited, links }`. Only called when the container is running;
|
||
skipped (muted badges) when stopped. Also accessible via the gateway
|
||
at `/agent/<name>/api/dashboard-state`.
|
||
- `GET /api/todos` — harness-local todos snapshot (loose-ends v2)
|
||
consumed by the todos flyout (`refreshTodos` / `buildTodosList`).
|
||
See the todos-flyout paragraph above for the producer/subsystem
|
||
model and the bulk mark-done row.
|
||
- `GET /api/stats?window=24h|7d|30d|all` — time-bucketed turn analytics
|
||
`Snapshot` consumed by the `/stats` page. `all` ranges from the
|
||
earliest recorded turn with an adaptive bucket width.
|
||
- `GET /icon` — agent's icon as `image/svg+xml`. Returns
|
||
`/etc/hyperhive/icon.svg` (set via `services.hyperhive.agent.icon` in `agent.nix`)
|
||
when present, otherwise **404** — there is no server-side default.
|
||
Consumers (dashboard container row, this page's own header icon)
|
||
hit `/icon` optimistically and fall back client-side on load failure
|
||
to the frontend-bundled `/favicon.svg` rather than probing first.
|
||
- `GET /events/history` — replay buffer for the terminal.
|
||
- `GET /screen` — VNC viewer page (minimal RFB-over-WebSocket
|
||
renderer — deliberately thin, just enough to display the
|
||
desktop + forward pointer + keyboard. A production-grade viewer
|
||
would vendor noVNC; this file ships the minimal in-tree variant).
|
||
Only accessible when `services.hyperhive.agent.gui.enable = true` in the agent's
|
||
`agent.nix`; the harness shows a 🖥 screen link in the state row
|
||
when `gui_vnc_port` is present. A `← agent` back-link in the page
|
||
header returns to the main per-agent page. Controls: `⤢ fit` CSS-downscales
|
||
the canvas to the window via `relayoutCanvas()` setting explicit
|
||
pixel dimensions on the canvas — *not* CSS `max-width/max-height`,
|
||
because a flex item's automatic minimum size (`min-width: auto`
|
||
resolves to the canvas's intrinsic framebuffer resolution) silently
|
||
clamps `max-*` back up, making fit mode a no-op that just centred +
|
||
clipped the oversized canvas. The fit-mode rules pin the canvas
|
||
with `flex: none; min-width: 0; min-height: 0` so the JS-set size
|
||
sticks. `⤡ match size` sends an RFB `SetDesktopSize` request so the
|
||
server (weston) changes its real output resolution to the window
|
||
dimensions; enabled once the server advertises the
|
||
`ExtendedDesktopSize` pseudo-encoding (`-308` rect in the header).
|
||
Fit-mode state persists in `localStorage` (`screen-fit`); default
|
||
is on. `sendPointer` rescales pointer coordinates so clicks
|
||
land on the right pixel regardless of CSS scale.
|
||
- `GET /screen/ws` — raw RFB byte relay: proxies WebSocket
|
||
frames to the weston VNC server at `127.0.0.1:<vnc_port>`.
|
||
Transparent to any RFB variant. VNC port comes from the
|
||
`HIVE_GUI_VNC_PORT` env var (a fixed port set on the harness
|
||
service when `services.hyperhive.agent.gui.enable`; see `weston-vnc.nix`).
|
||
- `GET|POST /extra/<name>/…` — **extra web proxies** declared via
|
||
`services.hyperhive.agent.extraWebProxies` in `agent.nix` (serialised to the
|
||
`HIVE_EXTRA_WEB_PROXIES` env var as a JSON object
|
||
`{"<name>": "<upstream_url>"}`). Each entry mounts a transparent
|
||
reverse-proxy at `/extra/<name>/` that forwards every request (method,
|
||
headers, body) to the configured upstream, strips hop-by-hop headers
|
||
on both sides, and buffers the full response body (MVP — SSE
|
||
connections will appear as one large response rather than streaming).
|
||
The `/extra/` namespace ensures user-declared proxies can never
|
||
conflict with native agent endpoints. Upstream values are either an
|
||
`http(s)://` URL (forwarded via `reqwest`) or a Unix domain socket,
|
||
spelled `unix:<path>` (for example `unix:/run/myapp/http.sock`) — dialed
|
||
directly with a raw HTTP/1.1 client per request, since `reqwest` has
|
||
no UDS transport. Implemented in `web_ui/proxy.rs`.
|
||
|
||
Bus events (new vocabulary on `/events/stream`):
|
||
|
||
- `status_changed { status }` — `online` / `rate_limited` /
|
||
`needs_login_idle` / `needs_login_in_progress`. Drives the
|
||
alive-badge. The harness sets `rate_limited` when it detects a
|
||
429 response and clears it when the retry sleep expires.
|
||
- `model_changed { model }` — drives the model chip.
|
||
- `effort_changed { effort }` — drives the effort picker chip.
|
||
Emitted from `Bus::set_effort` on `POST /api/effort`; applies on
|
||
the next session start.
|
||
- `token_usage_changed { ctx: TokenUsage, cost: TokenUsage }`
|
||
— drives the ctx + cost badges. Emitted from
|
||
`Bus::record_turn_usage` at turn-end; `ctx` is the last
|
||
inference's usage (current context size), `cost` is the
|
||
cumulative across every inference (the `result` event's
|
||
totals).
|
||
- `turn_state_changed { state, since_unix }` — drives the
|
||
state badge (`idle`/`thinking`/`compacting`).
|
||
|
||
## Stats page
|
||
|
||
`GET /stats` is a separate per-agent page (served by the
|
||
harness, linked from the per-agent page's `📊 stats →` and from
|
||
each dashboard container row). A `← live` back-link in the page
|
||
header returns to the main per-agent page. Turn analytics, read-only, from
|
||
`/state/hyperhive-turn-stats.sqlite`. `GET /api/stats?window=
|
||
24h|7d|30d|all` returns a time-bucketed `Snapshot`; the page renders
|
||
it with Chart.js (bundled into `stats.js` via esbuild — no CDN
|
||
dependency). Charts: turns, duration (p50 · p95 · avg), context
|
||
tokens, token cost per bucket, a **turns-by-model** stacked bar
|
||
(model choice drives token cost, so it sits directly under the
|
||
cost chart), doughnuts for tool / wake-source / result mix, and a
|
||
**result-trend** stacked bar — per-bucket `result_counts` so
|
||
error / rate-limit / compaction outcomes are visible over time
|
||
(the doughnut shows only the window total).
|
||
A **favorite tools** doughnut shows the most-run shell commands —
|
||
normalised `bash_commands` heads written per bash task by the
|
||
hive-bash-daemon capture: the basename of the *first real command*,
|
||
looking past `cd repo &&` prefixes, env-assignments, and
|
||
prefix-runners like `sudo` / `env` (so `cd /repo && cargo build`
|
||
records `cargo`, not `cd`). Read via `bash_breakdown`; the card
|
||
stays hidden until the agent has run a bash command (a missing
|
||
`bash_commands` table degrades to an empty list), so it never
|
||
renders an empty chart.
|
||
A summary chip row carries window totals, plus two
|
||
token-efficiency chips derived from the bucket sums: **cache
|
||
hit-rate** (`cache_read` over all input-side tokens) and
|
||
**tokens/turn**. When `reminder_stats` is present (fetched via
|
||
`ReminderRollup` RPC and merged into the snapshot in
|
||
`web_ui/stats.rs::api_stats`) three more chips appear: **reminders
|
||
scheduled / delivered / pending** for the window. When the
|
||
per-session capture has data, a **first-turn ctx** chip shows the
|
||
input tokens of the most recent fresh claude session's first turn —
|
||
a proxy for system-prompt + CLAUDE.md sprawl (a fresh session's first
|
||
turn pays the full static prefix uncached). It's derived in `stats.rs`
|
||
(`first_turn_ctx`: the first turn, `ORDER BY started_at LIMIT 1`, of
|
||
the latest `sessions` row in the window) and stays out of the JSON
|
||
until the `sessions` / `turn_stats.session_id` capture has rows — so
|
||
the chip stays hidden on older dbs (inert-until-capture).
|
||
`stats.rs` opens the sqlite db read-only and degrades to an
|
||
empty snapshot on any error — the page is decorative, never
|
||
authoritative.
|