diff --git a/TODO.md b/TODO.md index 073f74d..40515d7 100644 --- a/TODO.md +++ b/TODO.md @@ -76,6 +76,10 @@ how often the friction bites in normal use. Field is optional, ignored if the referenced id is unknown / cross- agent / out of retention. +## Telemetry + +- **Per-turn stats log**: persist one row per claude turn in a new sqlite table on the per-agent state dir (or the host broker DB, indexed by agent). Columns: `started_at`, `ended_at`, `duration_ms`, `model`, `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, `tool_call_count`, `tool_call_breakdown` (JSON: `{Read: 12, Bash: 3, ...}`), `bytes_streamed`, `wake_reason` (recv'd message / reminder / operator-kick / manual), `result_kind` (ok / cancelled / failed-mid-turn / compacted), `note` (e.g. failure reason). Powers: per-agent dashboards (avg turn time over time, tool-usage histogram, cost projections from token counts × model rate), debugging stuck loops (look for repeated identical wake_reason + zero tool calls), and operator-visible "this is what your spend looked like this week" rollups. Source data is already mostly in the harness's `TurnState` + the per-event bus; just needs a sink. Keep a retention sweep (host-side) so the table doesn't grow forever. + ## Bugs - **Post-rebuild system-message missed wake**: at 09:13:14 the dashboard showed `system → damocles container rebuilt` as ✓ delivered, but the agent harness never ran a turn for it (no claude invocation, no operator-visible activity). A subsequent `recv()` from inside the agent returned `(empty)`, confirming the message was popped + marked delivered server-side — yet drove no turn. Most likely cause: the agent_server `serve_agent_stdio` task is up and answering MCP/socket calls, but the `hive-ag3nt::serve` long-poll loop that drives `drive_turn` either died silently during rebuild or never restarted. Investigate: (a) does hive-ag3nt's serve loop survive `nixos-container update` cleanly, or does its tokio runtime get torn down mid-loop? (b) is there an early-exit path on a transient socket error during rebuild that drops the serve task without notifying the manager? (c) compare timeline with manager's own post-rebuild wake to see if this is rebuilt-agents-only or universal. Could be related to the `recv_blocking` fix in `e423d57` if the rebuild restarts the broker mid-subscribe. diff --git a/hive-ag3nt/assets/agent.css b/hive-ag3nt/assets/agent.css index ca61c2a..d8db0df 100644 --- a/hive-ag3nt/assets/agent.css +++ b/hive-ag3nt/assets/agent.css @@ -165,12 +165,38 @@ pre.diff { font-size: 0.78em; letter-spacing: 0.04em; } -.token-usage { - color: var(--muted); - font-size: 0.8em; +/* Context-window badge. Mirrors Claude Code's bottom-right "N tokens" + chip — single primary number (total prompt tokens in use), full + breakdown on hover. Sized/coloured like a peer of model-chip so + the state row reads as one row of chrome. */ +.ctx-badge { + display: inline-block; + padding: 0.1em 0.6em; + border: 1px solid var(--purple-dim); + border-radius: 999px; + color: var(--green); + font-size: 0.78em; letter-spacing: 0.04em; cursor: default; + white-space: pre-line; } + +/* Harness reachability badge. Same chip shape + sizing as + `.state-badge` / `.model-chip` so the state row stays visually + uniform; colour communicates the actual reachability state. */ +.status-badge { + display: inline-block; + padding: 0.25em 0.8em; + border: 1px solid; + border-radius: 999px; + font-size: 0.85em; + letter-spacing: 0.05em; +} +.status-badge.status-loading { color: var(--muted); border-color: var(--purple-dim); } +.status-badge.status-online { color: var(--green); border-color: var(--green); + text-shadow: 0 0 6px rgba(166, 227, 161, 0.55); } +.status-badge.status-needs-login { color: var(--amber); border-color: var(--amber); } +.status-badge.status-offline { color: var(--muted); border-color: var(--muted); } .btn-dashlink { color: var(--cyan); border: 1px solid var(--cyan); diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index 14d6167..375025e 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -91,10 +91,10 @@ document.title = `${label} // hyperhive`; } - function renderOnline(_label, root) { - root.append( - el('p', { class: 'status-online' }, '● harness alive — turn loop running'), - ); + function renderOnline(_label, _root) { + // Online state is conveyed by the `#alive-badge` chip in the + // state row — no longer a separate paragraph in the status + // block (keeps the terminal the star, status row stays compact). } function renderNeedsLoginIdle(root) { @@ -348,6 +348,13 @@ const h = Math.floor(m / 60); return h + 'h ' + (m % 60) + 'm'; } + const STATE_TOOLTIPS = { + loading: 'harness not yet contacted', + offline: 'harness unreachable or claude not logged in', + idle: 'turn loop running, no claude invocation in flight', + thinking: 'claude is executing the current turn', + compacting: 'operator-triggered /compact running on the persistent session', + }; function renderStateBadge() { const badge = $('state-badge'); if (!badge) return; @@ -355,6 +362,7 @@ const age = fmtAge(Date.now() - stateSince); badge.textContent = def.glyph + ' ' + def.text + ' · ' + age; badge.className = 'state-badge state-' + stateName; + badge.title = (STATE_TOOLTIPS[stateName] || '') + '\nin this state for ' + age; const cancelBtn = $('cancel-btn'); if (cancelBtn) cancelBtn.hidden = stateName !== 'thinking'; } @@ -405,27 +413,53 @@ list.append(li); } } + // Harness reachability badge: derived from the same `s.status` the + // status block reads. Each status maps to a glyph + label + colour + // class. Lives in the state row so the operator sees boot/login/ + // online without losing terminal real-estate to a paragraph. + const ALIVE_LABELS = { + loading: { glyph: '…', text: 'connecting', cls: 'status-loading' }, + online: { glyph: '●', text: 'alive', cls: 'status-online' }, + needs_login_idle: { glyph: '◌', text: 'needs login', cls: 'status-needs-login' }, + needs_login_in_progress: { glyph: '◌', text: 'logging in', cls: 'status-needs-login' }, + offline: { glyph: '○', text: 'offline', cls: 'status-offline' }, + }; + function renderAliveBadge(status) { + const el_ = $('alive-badge'); + if (!el_) return; + const def = ALIVE_LABELS[status] || ALIVE_LABELS.loading; + el_.textContent = def.glyph + ' ' + def.text; + el_.className = 'status-badge ' + def.cls; + } + function renderModelChip(model) { const el_ = $('model-chip'); if (!el_) return; if (!model) { el_.hidden = true; return; } el_.hidden = false; el_.textContent = 'model · ' + model; + el_.title = `claude --model ${model}\nset via the operator's /model command; persists across turns until changed`; } + // Context badge — mirrors Claude Code's bottom-right "N tokens" + // indicator. Primary number is total prompt tokens used in the + // current context window (input + both cache axes); hover for the + // breakdown including output. Kept as chrome on the state row so + // the terminal stays the star. function renderTokenUsage(u) { - const el_ = $('token-usage'); + const el_ = $('ctx-badge'); if (!el_) return; if (!u) { el_.hidden = true; return; } const ctx = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens; const fmt = (n) => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); el_.hidden = false; el_.title = [ + 'context window in use', 'input: ' + u.input_tokens, - 'output: ' + u.output_tokens, 'cache_read: ' + u.cache_read_input_tokens, 'cache_write: ' + u.cache_creation_input_tokens, - ].join(' · '); - el_.textContent = '· ctx ' + fmt(ctx) + ' in · ' + fmt(u.output_tokens) + ' out'; + 'output (last turn): ' + u.output_tokens, + ].join('\n'); + el_.textContent = 'ctx · ' + fmt(ctx); } function renderLastTurn(ms) { const el_ = $('last-turn'); @@ -435,6 +469,7 @@ else if (ms < 60_000) s = (ms / 1000).toFixed(1) + 's'; else s = Math.floor(ms / 60_000) + 'm ' + Math.floor((ms / 1000) % 60) + 's'; el_.textContent = '· last turn ' + s; + el_.title = `wall-clock duration of the last completed claude turn (${ms} ms)`; el_.hidden = false; } function startStateTicker() { @@ -499,6 +534,7 @@ } else if (s.turn_state) { setStateAbs(s.turn_state, s.turn_state_since); } + renderAliveBadge(s.status); renderModelChip(s.model); renderTokenUsage(s.token_usage); // Skip the re-render if nothing structurally changed. The most diff --git a/hive-ag3nt/assets/index.html b/hive-ag3nt/assets/index.html index 6180184..204ec13 100644 --- a/hive-ag3nt/assets/index.html +++ b/hive-ag3nt/assets/index.html @@ -14,10 +14,11 @@