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.
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):
HIVE_CONTEXT_WINDOW_TOKENS_<KEY>env var, whereKEY(lowercased) is a substring of the active model name. Injected by the meta flake fromservices.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.HIVE_CONTEXT_WINDOW_TOKENS— single global override for any model (useful in dev / test).- 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:
- Reactive — claude-code prints
Prompt is too long. The session is already past the window, so no turn can run on it — the session/compacts 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,runsurfacesError::PromptTooLong;drive_turnthen 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 — 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/statenow") and then/compacts, 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:
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% ofcontext_window_tokens(model)) AND the time since the last turn exceeds the assumed prompt-cache TTL (HIVE_CACHE_TTL_SECS, default3600). 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. Sodrive_turnarchives 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. SetHIVE_AUTO_RESET_WATERMARK_TOKENS=0to 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_ag3nt::turn::write_* writes two files next to the per-agent
socket at /run/hive/ once at startup:
-
claude-mcp-config.json— points claude at the persistenthive-mcp-httpdaemon (http://127.0.0.1:{port}/mcp, port fromhyperhive.mcp.httpPort, default 8790) for the built-inhyperhivesurface. 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 fromhive-ag3nt/prompts/system.mdbyhive_ag3nt::prompt::render: HTML-comment markers (<!-- role:agent -->...<!-- /role:agent -->, same forrole:manager) gate the role-specific blocks; everything else is shared. Five placeholders are then substituted:{label}(short agent name),{qualified_label}(hive-qualifiedname@domainform),{operator_pronouns},{hive_identity}(e.g.on hive `pr1ma`; empty whenhyperhive.hiveNameis unset), and{swarm_identity}(same shape for the swarm). Pronouns come fromHIVE_OPERATOR_PRONOUNSenv (set by the meta flake fromservices.hyperhive.c0re.operatorPronouns, defaultshe/her). Whenhyperhive.docs.enableis set,HIVE_DOCS_DIRis present in the environment andrender()appends a one-sentence pointer telling the agent the docs are mounted at that path (in lieu of the old CLAUDE.md-in-docs-dir approach, which was dropped in favour of this direct injection). Passed via--system-prompt-file.Marker grammar.
<!-- role:X -->opens a block; any<!-- /role:X -->closes the current block. The renderer always uses roleagent, 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'ssystem.mdcarries no markers (single agent role) — the grammar stays wired for a future manager / multi-role prompt.hive_identity/swarm_identityshape. 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 treatsSome("")from a caller asNone` so empty-string env vars and missing env vars round-trip the same way.
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.