251 lines
14 KiB
Markdown
251 lines
14 KiB
Markdown
# 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-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.
|
|
|