The claude invocation

claude --print --verbose --output-format stream-json --model <name> \
  --effort <level> --resume <title>   # or --name <title> on first use \
  --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

Crate split. The generic subprocess mechanics — spawning claude --print, streaming + classifying stream-json, session lookup/archive, and the durable-session compaction loop — live in the reusable hive-claude crate (hive_claude::{Claude, InfiniteSession, Attach, CompactionPolicy, PercentPolicy, Telemetry, Sink, SessionStore}; see hive-claude/README.md). hive_ag3nt::turn is the hyperhive policy layer on top: it builds the per-turn config from the bus, bridges the output stream onto the event bus (BusSink), and owns the compaction / auto-reset / retry decisions in drive_turn. The lib returns everything it parsed from a turn (usage, cost, context window, resolved model) as Telemetry, which the policy layer applies to the bus.

Which claude binary. The bare name claude, resolved off the harness unit's PATH. By default that's the claude-code in the agent's own nixpkgs (the meta flake's nixpkgs input) via environment.systemPackages. Since that's usually a release channel and this package moves fast, the operator can pin one hive-wide with services.hyperhive.c0re.claudeCodePackage: its store path is written into each agent's flake, and claude on PATH becomes a symlink to it instead of the container's own claude-code — so there's only ever one claude in the container. Agents pick up a new build on their next rebuild, not live. See docs/gotchas.md::claude-code is unfree.

Hive-enforced settings ship at /etc/claude-code/managed-settings.json (claude-code's canonical managed-settings path — precedence #1, read-only, un-overridable), wired in nix/agent-modules/claude-settings.nix from the prompts/claude-settings.json asset. effortLevel is deliberately not in that file — effort is controlled live via the --effort flag (HIVE_DEFAULT_EFFORT / the per-agent UI slider), which managed scope would otherwise lock.

<name> is read from Bus::model() on each turn. The initial default is set by hyperhive.model in the agent's agent.nix (NixOS option; propagates via HIVE_DEFAULT_MODEL env var; falls back to "haiku" if unset). The operator can flip it at runtime with /model <name> in the web terminal — the next turn picks it up. The choice is persisted to /harness/hyperhive-model so it survives restart; override path: HYPERHIVE_MODEL_FILE env var for tests.

Context-window size is looked up per-model via events::context_window_tokens(model). Resolution order (first match wins):

  1. HIVE_CONTEXT_WINDOW_TOKENS_<KEY> env var, where KEY (lowercased) is a substring of the active model name. Injected by the meta flake from services.hyperhive.c0re.contextWindowTokens (host-level NixOS option, defaults: haiku=200k, sonnet=1M, opus=1M). Override these for all agents at once without a per-agent config change.
  2. HIVE_CONTEXT_WINDOW_TOKENS — single global override for any model (useful in dev / test).
  3. Hard fallback: 200_000 (conservative; only reached outside NixOS where the env vars aren't set).

The effective window drives watermarks and is exposed at runtime via /api/state.context_window_tokens so the UI can show a percentage-of-window ctx badge.

Session identity — a constant title. Every turn keys on one fixed, harness-owned session title (turn::session_title(), default hive-session, override HIVE_SESSION_TITLE). The durable hive_claude::InfiniteSession (built once by the serve loop via turn::make_session, then reused) --resume <title>s it; the first use (bootstrap, post-archive, post-purge) misses and the session re-runs the same prompt once with --name <title> to mint it. That single self-heal rule is the whole identity system — there is no scraped session-id file. Because the title is constant, /compact and its post-compact retry provably target the same session (killing the old "compact ran on a different/empty session" bug), and a choom invocation in the same cwd can't hijack the context (it won't carry our title). claude stores sessions in ~/.claude/projects/<cwd-slug>/<uuid>.jsonl (bind-mounted persistently); --name writes the title into the file as a custom-title event, which is what --resume <title> resolves against. We never pass bare --continue (it resumes the latest session in the cwd — the hijack vector). Auto-compact, auto-memory, and dynamic workflows (the /workflows feature) are disabled via the managed settings at /etc/claude-code/managed-settings.json: hyperhive owns compaction (see Compaction below), and disableWorkflows keeps the /workflows machinery from spawning sub-runs that burn usage on the harness's autonomous turns.

Session reset is available via POST /api/new-session (or /new-session slash command). It does not touch the session inline — that would race a mid-write claude process. Instead Bus::request_session_reset() sets a one-shot flag consumed at the next turn boundary by drive_turn, which archives the current session: the backing <uuid>.jsonl is renamed to <uuid>.jsonl.archived (dropped out of claude's *.jsonl resolution glob, history preserved on disk, only the file carrying our title — any choom session sharing the cwd is left alone). The next turn's --resume <title> then misses and self-heals into a fresh session.

Compaction

claude's own in-session auto-compact is off (via the managed settings at /etc/claude-code/managed-settings.json); hyperhive owns it. The hive_claude::InfiniteSession keeps the session alive across the context window with two triggers baked into its run:

The when is a hive_claude::CompactionPolicy injected by the harness: turn::make_session builds a PercentPolicy that compacts once the model-reported context fill reaches HIVE_COMPACT_WATERMARK_PERCENT (default 75%), falling back to events::context_window_tokens(model) for the window on turns the model didn't report one. 0 disables proactive compaction (the reactive path always applies). The proactive path is best-effort — a failed checkpoint or /compact never fails the turn that already succeeded.

The operator can force a compaction any time via POST /api/compact. It's deferred: the handler sets Bus::request_compact() and returns immediately; the harness runs the /compact at the next turn boundary (end of the in-flight turn in drive_turn, or — when the agent is idle — in turn::run_pending_compact on the serve loop's next empty poll). This lets /api/compact work mid-turn instead of only when idle, without racing a live claude process.

To disable proactive compaction for a specific agent, use the nix option:

hyperhive.autoCompact = false;  # default true

Setting autoCompact = false sets HIVE_COMPACT_WATERMARK_TOKENS=0, which the percent resolver still honours as a disable. Useful for large-context models (sonnet/opus) where the 75% heuristic fires before the session is actually full — the reactive path (compact-on-overflow at the hard limit) still applies.

The child runs with cwd = /state (when the bind exists; falls back to the parent's cwd in dev), so any relative path in a tool call (Read foo.md, Bash ls, Write notes.md) lands in the agent's durable bind-mounted dir. CLAUDE.md auto-load walks upward from /state — drop a per-agent CLAUDE.md there if you want long-term hints that survive destroy/recreate.

The wake prompt is intentionally minimal: the popped message's from/body, prefixed with a [msg #<id>] broker-row-id marker (so the agent knows what id to pass to ack_until for bulk triage; transient pings show no marker because their sentinel id 0 has nothing to ack), plus an inline ({unread} more pending — drain via …) hint when unread > 0. Claude drives any further recv/send itself via the embedded MCP server.

Whenever hive-c0re starts / restarts / rebuilds a container, it also drops a system message into the agent's inbox via Coordinator::kick_agent — a one-line "you were just (re)started, check /state/ for your notes, your session is intact". The next turn picks it up like any other inbox message.

On-boot files

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

The per-turn plumbing lives in hive_ag3nt::turn: write_mcp_config / write_system_prompt (on-boot files), make_session (builds the durable InfiniteSession, once), drive_turn (the policy state machine — reset/auto-reset, the turn, 401-retry, deferred-compact-at-turn-end), run_pending_compact (idle operator compact), BusSink (stream → bus + Telemetry applied via apply_telemetry), emit_turn_end, session_title / session_store / archive_session (identity + turn-boundary reset). The actual claude spawn, stream classification, and the reactive/proactive compaction loop are in the hive-claude crate. Login-wait (wait_for_login) lives in hive_ag3nt::login.