# The claude invocation ``` claude --print --verbose --output-format stream-json --model \ --effort --resume # 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-agent`'s `turn` module 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](#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`: - **Reactive** — claude-code prints `Prompt is too long`. The session is *already* past the window, so no turn can run on it — the session `/compact`s straight away and retries the same wake-up prompt once. No notes-checkpoint turn is possible here: the detail is gone. If the retry *still* overflows, `run` surfaces `Error::PromptTooLong`; `drive_turn` then archives the session (session lifecycle stays hive-side) and the serve loop requeues the message so it redelivers into a fresh session (see [Turn outcomes](README.md#turn-outcomes) — the wake prompt itself is tiny, so the overflow was the accumulated context the archive clears). - **Proactive** — a turn finishes cleanly but the last inference's context size crossed the policy watermark. While the session is still healthy it runs one synthetic *notes-checkpoint* turn (`CHECKPOINT_PROMPT` — "context is filling up, flush durable state into `/state` now") and *then* `/compact`s, so the agent can persist in-flight state before the detail collapses into a summary. 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: ```nix 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. - **Auto session-reset** — a third path (`turn::maybe_auto_reset`, pre-turn) that fires when both conditions hold: context is ≥ a watermark (`HIVE_AUTO_RESET_WATERMARK_TOKENS`, default **50% of `context_window_tokens(model)`**) AND the time since the last turn exceeds the assumed prompt-cache TTL (`HIVE_CACHE_TTL_SECS`, default `3600`). Claude's prompt cache goes cold after a while; once it's cold, `--resume`-ing a large session pays the full re-upload cost with no benefit over starting fresh. So `drive_turn` **archives** the current session (same mechanism as the operator reset — rename `<uuid>.jsonl` → `.archived`) so the next turn's `--resume <title>` misses and starts fresh. Unlike proactive compaction the session is dropped entirely, not compacted — and *no* preceding checkpoint turn runs, because any turn before the reset would just re-warm the cache and defeat the purpose. Set `HIVE_AUTO_RESET_WATERMARK_TOKENS=0` to disable. Auto-reset and the operator reset are mutually exclusive per turn (both archive → fresh turn), so an explicit operator reset short-circuits the heuristic. 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-agent`'s `turn` module writes two files next to the per-agent socket at `/run/hive/` once at startup: - `claude-mcp-config.json` — points claude at the persistent `hive-mcp-http` daemon (`http://127.0.0.1:{port}/mcp`, port from `hyperhive.mcp.httpPort`, default 8790) for the built-in `hyperhive` surface. HTTP is the sole transport for it — no per-turn stdio child, so the URL survives the per-turn claude re-spawn (no re-registration race), trading that for a hard dependency on the daemon's uptime (`Restart=always`, no stdio fallback). Extra servers stay stdio. - `claude-system-prompt.md` — rendered from `hive-agent/prompts/system.md` by `hive-agent`'s `prompt::render`: HTML-comment markers (`<!-- role:agent -->...<!-- /role:agent -->`, same for `role:manager`) gate the role-specific blocks; everything else is shared. Five placeholders are then substituted: `{label}` (short agent name), `{qualified_label}` (hive-qualified `name@domain` form), `{operator_pronouns}`, `{hive_identity}` (e.g. `` on hive `pr1ma` ``; empty when `hyperhive.hiveName` is unset), and `{swarm_identity}` (same shape for the swarm). Pronouns come from `HIVE_OPERATOR_PRONOUNS` env (set by the meta flake from `services.hyperhive.c0re.operatorPronouns`, default `she/her`). When `hyperhive.docs.enable` is set, `HIVE_DOCS_DIR` is present in the environment and `render()` appends a one-sentence pointer telling the agent the docs are mounted at that path. Passed via `--system-prompt-file`. **Marker grammar.** `<!-- role:X -->` opens a block; any `<!-- /role:X -->` closes the current block. The renderer always uses role `agent`, so blocks with other role tags are elided. Nesting is NOT supported — a stray opener with no closer runs until end of file. Whitespace inside markers is tolerated (`<!--role:foo-->` parses the same as `<!-- role:foo -->`). Content outside any marker is always included. Today's `system.md` carries no markers (single agent role) — the grammar stays wired for a future manager / multi-role prompt. **`hive_identity` / `swarm_identity` shape.** Each carries a leading space + backticked name (` on hive \`pr1ma\``, ` in swarm \`constellat1on\``) when the corresponding env var is set, otherwise empty string. The independence lets the template drop one or both into the opener prose without breaking single-hive deployments that never set the option; the renderer also treats `Some("")` from a caller as `None` so empty-string env vars and missing env vars round-trip the same way. The per-turn plumbing described on this page — on-boot files, session identity, the reset/auto-reset/retry state machine, and the telemetry-to-bus bridge — lives in `hive-agent`'s `turn` module; see its `//!` doc comment (`hive-agent/src/turn.rs`) for the exact call shape. The actual claude spawn, stream classification, and the reactive/proactive compaction loop are in the `hive-claude` crate. Login-wait lives in `hive-agent`'s `login` module.