| Filename | Latest commit message | Latest commit date |
|---|---|---|
Third and last of #2860's agent-facing URL fallbacks. The operator's ruling was "any special casing is done on the nix side - same binaries, no hard coded fallback", so the default is deleted rather than replaced. Every layer guessed the same wrong thing, and each guess was only ever correct for a process sharing the host netns: - nix/agent-modules/matrix.nix: matrixUrlDefault = localhost:8008, both as the option's default and as a sentinel the daemon unit compared against to decide whether to write HIVE_MATRIX_URL. Now nullOr str, default null, the guard is != null, and the doc says what forge.url's already says: null means "no matrix", not "guess one". - nix/host-modules/hive-c0re/environment.nix: forwarded http://127.0.0.1:<port> when no gatewayHost was set. hive-c0re shares the host netns so it reads as harmless, but the value is handed to agents, which do not -- there it names the agent itself. Now forwarded only when there is a gateway vhost to name, matching the guard HIVE_MATRIX_PUBLIC_URL already uses twelve lines below. - hive-matrix-mcp: paths::DEFAULT_HOMESERVER was the same address compiled in, so dropping the nix defaults alone would have left the daemon dialling loopback inside the agent's own netns -- the very bug, one layer down. homeserver_url() is now Option, and an account with no homeserver is skipped with a log, exactly as one with no token is. discover_token_accounts already refused to guess for the same reason. Two comments taught the assumption back to the next reader ("shared host netns means every agent container resolves localhost to the same machine"); both now say which side of the netns boundary they describe. MATRIX_HTTP keeps its value -- hive-c0re really does share the host netns -- but no longer claims agents do. Gated with nix eval against the extended agent-base config, as a pair: with no url set the daemon unit carries no HIVE_MATRIX_URL, and with one set it carries exactly that. Either check alone passes on a broken guard. |
||
| .. | ||
| claude-invocation.md | ||
| config.md | ||
| mcp.md | ||
| README.md | ||
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-agent — one serve-loop binary for all
agents) runs:
- Check the pause marker (
<harness>/paused). While it exists the loop does nothing but re-stat it every 5 s — no broker poll, no claude process. Because step 1 is never reached, messages stay queued and unacked, so a resume drains the backlog instead of losing it; reminders and todo wakes buffer in their channels. Set it withhivectl agent <name> pauseor the dashboard toggle; see persistence. - Long-poll
Recvon its socket. The host-side broker (broker.rs::recv_blocking_batch) returns immediately if there's a pending message, otherwise waits up to 30 s for a brokerSentevent for this recipient. - Pop one message. Peek the remaining inbox depth with
Status. - Emit
LiveEvent::TurnStart { from, body, unread }onto the SSE bus. - Spawn claude (one process per turn) and pipe the wake prompt over stdin.
- Stream stdout (JSON lines) into the bus as
LiveEvent::Stream(value). Pump stderr asNote. - Wait for claude to exit and classify the turn's outcome from the stream + exit — success, compaction, rate-limit, auth-failure, or hard failure. The outcome drives the post-turn action (see Turn outcomes); compaction is handled inside the session (see Compaction). Rate-limit and auth-failure detection is described below.
- Emit
LiveEvent::TurnEnd { ok, note }. Sleeppoll_msto avoid tight loops on transient failures.
Failure detection and login
- Rate limit — a
429/rate_limitmarker on stderr, or a parsed{"type":"error"}rate-limit event on stdout (conversation-text mentions don't count), sets therate_limitedsentinel, parks forHIVE_RATE_LIMIT_SLEEP_SECS(default 300), then retries. The UI shows a⊘ rate limitedbadge while parked. - Auth failure (401) —
drive_turnretries once (transient token-refresh races clear on retry); a secondAuthFailedwrites{state_dir}/hyperhive-needs-login, requeues the message, and parks inwait_for_login— the same path as a cold boot with no session. The operator re-auths via the per-agent web UI; the queued message then drives the next turn. - Login detection — both boot (
login::has_session, Online vs NeedsLogin) andwait_for_login's resume check key off the credential files inlogin::CRED_FILE_NAMES(the set/logoutdeletes).wait_for_loginresumes only when that set changes (a new file or a newer mtime), so stale credentials on disk at the 401 don't trigger an instant false-resume, and leftover session-history files don't read as a live session after a logout + container recreate.
Harness binary shape
Two sibling binaries out of the one hive-ag3nt crate, all
role-agnostic. (The earlier split into hive-ag3nt + hive-m1nd
was collapsed because the privilege boundary lives server-side at
the broker socket (/run/hive/mcp.sock): ManagerRequest calls are
refused by the standard agent socket regardless of who sends them.)
hive-agent— long-running harness loop (the inbox poll + claude-pump + ack/requeue cycle described above).hive-agent-mcp— MCP server for the built-inhyperhivesurface. Run with--http <addr>as a persistent streamable-HTTP daemon (thehive-mcp-httpsystemd unit, onhyperhive.mcp.httpPort, default 8790); claude connects to its URL via--mcp-config. HTTP is the sole transport — no per-turn stdio child (eliminates the re-registration race).
Surface trait + zero-sized type tags
AgentRequest / AgentResponse (= ManagerRequest / ManagerResponse —
type aliases) are the wire types. There is one role: agent.
bin/hive-agent.rs factors the turn loop through a Surface trait
with one zero-sized impl (AgentSurface) wrapping:
- One async method per wire op:
ack_turn,requeue_inflight,inbox_unread,post_turn_counts,send_to_parent,recv_next.
main() calls serve_main::<AgentSurface> for all roles. The turn
loop (serve_loop / handle_turn) has no per-role branches.
Boot wiring
serve_main reads HIVE_PORT (default DEFAULT_WEB_PORT) +
HIVE_LABEL (default "hive" for standalone runs; the meta
flake sets it unconditionally for any container-deployed agent;
see docs/conventions.md::Hive identity for the env stack),
opens turn-stats sqlite, prepares the on-boot files (see
claude-invocation),
installs claude plugins, spawns web_ui::serve + vacuum::run,
and either drops into serve_loop directly (Online) or parks on
the login flow first (NeedsLogin). (The forge notification poller
used to be spawned here too; it is its own process now —
hive-forge-notify, see forge.md.)
spawn_todo_socket opens the todos store and, alongside
todo_server::run (the socket the out-of-process producers dial),
spawns disk_watch::run — an in-process todo producer, so it shares
the store + wake Notify directly rather than dialling its own socket.
It raises a keyed disk todo when the filesystem backing the agent's
state gets tight, naming the agent's own biggest directories; the
summary is bucketed and carries no raw byte counts, so an unchanged
situation re-upserts as changed == false and never re-wakes.
Plugin install failures are not fatal: each entry comes back as a
human-readable failure string that gets routed via
Surface::send_to_parent to the agent's topology parent (the
broker resolves <parent> per topology::parent_of; root agents
and the manager fall through to operator).
Turn outcomes
turn::TurnOutcome (Result<bool, TurnError> — Ok(compacted) on success,
else a TurnError) drives the post-claude branch:
| Outcome | Action |
|---|---|
Ok(_) (false normal / true compacted) |
ack_turn |
Err(PromptTooLong) |
drive_turn archived the session (the lib already compacted + retried and it still overflowed); requeue inflight so the message redelivers into a fresh session that fits — no status park |
Err(RateLimited) |
sleep HIVE_RATE_LIMIT_SLEEP_SECS (default 300), requeue inflight, status back to online |
Err(AuthFailed) |
emit needs_login_idle sentinel, requeue inflight, park in wait_for_login |
Err(SessionNotFound) |
resume + create self-heal both missed ("shouldn't happen"); requeue inflight so the next turn creates fresh — no status park, message not dropped |
Err(ApiStall) |
idle watchdog killed claude after HIVE_TURN_IDLE_SECS (default 600) of output silence; sleep HIVE_STALL_SLEEP_SECS (default 60), requeue inflight, status back to online |
Err(Failed(err)) |
route [system] \` claude turn failed:\ntoviasend_to_parent` |
ApiStall catches an Anthropic API stall — a multi-retry connection storm where
the stream goes silent for minutes. The idle watchdog lives in hive-claude's
driver (Config::idle_timeout, enforced around child.wait()): the timer
resets on every stdout line, so a large but still-streaming turn is never cut —
only complete output silence for the window trips it. The harness sets the
window from HIVE_TURN_IDLE_SECS (0 disables) and maps the driver's
Error::IdleTimeout onto TurnError::ApiStall.
After the outcome handler, the stats sink records a row. handle_turn
reports the result to serve_loop via TurnControl { auth_failed } —
on auth failure the loop parks in wait_for_login; otherwise it loops
straight back to the idle wait (step 1). There is no same-turn
self-continue mechanism: every multi-step continuation rides an
external wake instead — a new inbox message, a remind, or an
in-container todo wake (bash-task completion, forge notification,
matrix activity). Ending the turn and letting one of
those drive the next one is strictly better than parking in-process:
it checkpoints the session and observes wakes that only reach the
harness between turns.
Sub-pages
The rest lives alongside this page, in three topic files:
- claude-invocation.md — how the harness
spawns
claude --printeach turn, the two-pronged compaction (reactive + proactive), and the on-boot files it materialises (--mcp-config,--system-prompt-file). - config.md — the optional per-agent knobs the meta flake wires in (reference docs, icon, passwordless sudo, dashboard links, custom static files, connectivity overrides, claude plugins, cargo message filtering).
- mcp.md — the MCP tool surface claude sees: core tools, privileged tool groups, self-wake, authoritative state, the tool envelope, and the built-in tool whitelist.
Per-subsystem impl detail lives in each module's //! doc-comment; these pages
describe present-state behaviour + wiring, not line-level mechanics.