hyperhive/docs/turn-loop.md
müde 8b10731aa4 split claude.md into docs/ — per-topic, human-readable
claude.md was eating 400 lines of subsystem detail that's useful
when you're working on that subsystem and noise the rest of the
time. split into:

- docs/conventions.md   naming, identity, async forms, commit style
- docs/gotchas.md       nspawn / nixos-container quirks
- docs/web-ui.md        dashboard + per-agent layouts and endpoints
- docs/turn-loop.md     claude invocation, wake prompt, mcp surface
- docs/approvals.md     approval flow, manager policy, helper events
- docs/persistence.md   sqlite dbs, retention, state dir layout

claude.md is now the entry point — file map, reading paths
("pick the doc that matches your task"), quick reminders that
fit on one screen, and a small scratchpad section for in-flight
context. references the docs; the docs don't reference claude.md.

no content was lost — the docs/ files cover everything the old
claude.md did, plus things i wrote up better while extracting.
2026-05-15 20:17:11 +02:00

4.7 KiB

Turn loop + MCP

How the harness wakes up, what it asks claude to do, and what tools claude has access to in return.

The loop

Each agent harness (hive-ag3nt serve or hive-m1nd serve) runs:

  1. Long-poll Recv on its socket. The host-side broker (broker.rs::recv_blocking) returns immediately if there's a pending message, otherwise waits up to 30 s for a broker Sent event for this recipient.
  2. Pop one message. Peek the remaining inbox depth with Status.
  3. Emit LiveEvent::TurnStart { from, body, unread } onto the SSE bus.
  4. Spawn claude (one process per turn) and pipe the wake prompt over stdin.
  5. Stream stdout (JSON lines) into the bus as LiveEvent::Stream(value). Pump stderr as Note.
  6. Wait for claude to exit. On Prompt is too long, run /compact on the session once and retry the turn.
  7. Emit LiveEvent::TurnEnd { ok, note }. Sleep poll_ms to avoid tight loops on transient failures.

The claude invocation

claude --print --verbose --output-format stream-json --model haiku \
  --continue --settings /run/hive/claude-settings.json \
  --system-prompt-file /run/hive/claude-system-prompt.md \
  --mcp-config /run/hive/claude-mcp-config.json --strict-mcp-config \
  --tools <builtins> --allowedTools <builtins+mcp>
# wake prompt piped over stdin

--continue keeps a persistent session per agent (claude stores sessions in ~/.claude/projects/, which is bind-mounted persistently). Auto-compact and auto-memory are disabled via --settings because hyperhive owns compaction (/compact on overflow, retry once; operator can also force one via /api/compact).

The wake prompt is intentionally minimal: just the popped message's from/body, plus an inline ({unread} more pending — drain via …) hint when unread > 0. Claude drives any further recv/send itself via the embedded MCP server.

On-boot files

hive_ag3nt::turn::write_* writes three files next to the per-agent socket at /run/hive/ once at startup:

  • claude-mcp-config.json — re-invokes the running binary as mcp child (so the same binary serves as harness + as claude's MCP child process).
  • claude-settings.json — the --settings blob (auto-compact and auto-memory off, effortLevel medium).
  • claude-system-prompt.md — rendered from hive-ag3nt/prompts/{agent,manager}.md with {label} substituted. Passed via --system-prompt-file.

The shared per-turn plumbing lives in hive_ag3nt::turn::{write_mcp_config, write_settings, write_system_prompt, run_turn, drive_turn, emit_turn_end, wait_for_login, compact_session} so the two binaries can't drift.

MCP surface

The harness ships an embedded MCP server (rmcp 1.7). Claude launches it as a stdio child via --mcp-config. The hyperhive socket name is hyperhive, so the tools land in claude as mcp__hyperhive__<tool>.

Sub-agent tools

  • send(to, body) — message a peer (logical agent name), another agent, or the operator (recipient operator, surfaces in the dashboard inbox).
  • recv() — drain one inbox message.

Manager tools (in addition to send/recv)

  • request_spawn(name) — queue a Spawn approval for a brand-new sub-agent (≤9 char name). Operator approves on the dashboard.
  • kill(name) — graceful stop. No approval required.
  • start(name) — start a stopped sub-agent. No approval.
  • restart(name) — stop + start. No approval.
  • request_apply_commit(agent, commit_ref) — submit a config change for any agent (hm1nd for the manager's own config) for operator approval.
  • ask_operator(question, options?, multi?) — surface a question on the dashboard. Non-blocking — returns the queued question id; the operator's answer arrives later as HelperEvent::OperatorAnswered in the manager inbox. Options always render alongside a free-text fallback; multi=true renders options as checkboxes.

The boundary: lifecycle ops on existing sub-agents (kill/start/restart) are at the manager's discretion — no operator approval. Creating a new agent (request_spawn) and changing any agent's config (request_apply_commit) still go through the approval queue.

Tool envelope

mcp::run_tool_envelope: every MCP tool handler logs the request, runs the body, logs the result. Pre-/post-log only — the inbox status hint moved to the wake prompt + UI header.

Tool whitelist (mcp::ALLOWED_BUILTIN_TOOLS)

  • Allowed built-ins: Bash, Edit, Glob, Grep, Read, TodoWrite, Write.
  • Denied by omission: WebFetch, WebSearch, Task, NotebookEdit.
  • Allowed MCP tools: as listed above per flavor.

Bash is on the allow-list pending a finer-grained pattern allow-list (Bash(git *)-style) — see TODO.