The harness runs as the non-root agent user; the per-agent
/run/hive-agent/<name>/ dir lands at 0755 root:root after
create_dir_all, so bind(2) of web.sock failed with EACCES. The error
was invisible because the web_ui::serve future was tokio::spawn'd with
its JoinHandle dropped — no log, no socket, agent looks unreachable
through the gateway.
closes#788. drops the per-role HIVE_LABEL fallback const ('hive-ag3nt'
on AgentSurface, 'hm1nd' on ManagerSurface) and replaces both with a
single literal 'hive' at the env-var unwrap site. real deploys set
HIVE_LABEL unconditionally via the meta-flake envelope; the fallback
is unreachable in production and there's no semantic reason for it to
differ per role.
nix-side standalone-eval fallback (HIVE_LABEL = 'hm1nd' in the
manager systemd unit) stays — that's wired so 'nixos-rebuild against
nixosConfigurations.manager' produces a sensibly-labelled container
even without the meta-flake wrapper.
Surface trait now: FLAVOR + FORGE_IS_MANAGER + 7 async wire methods.
next steps per #778 roadmap: #691 → #786 → #789.
mara on #778: 'remove the manager special case argus nitted about'.
`plugins::install_configured` no longer takes a `notify_recipient`
hardcoding "manager". Now returns a Vec<String> of failure messages;
serve_main<S> iterates them and routes each through S::send_to_parent
— the same <parent> sentinel failure-notify uses everywhere else
(#703). Manager plugin failures now reach operator via root → operator
fallback (improvement on the pre-PR silent-drop).
Also rename FORGE_MENTIONS_ONLY → FORGE_IS_MANAGER to fix the misnomer:
the boolean picks which wire enum (AgentRequest::Wake vs
ManagerRequest::Wake) the forge_notify poller uses, not anything about
mentions-only filtering (that's a separate nix-side option). Real fix
is to lift Surface into the lib crate and make forge_notify::run
generic; deferred to its own issue.
Net: -20 LOC.
mara on #778: "I would have expected the manager and agents to share
the exact same turn function, making one obsolete. I don't see that
in the code, why not?" — fair. went further.
introduces a Surface trait + AgentSurface / ManagerSurface zero-sized
impls wrapping the disjoint Request/Response enums + boot-time
constants (FLAVOR / DEFAULT_LABEL / PLUGINS_PARENT / FORGE_MENTIONS_ONLY).
the turn loop itself collapses to one generic implementation:
- serve_main<S> replaces agent_serve_main + manager_serve_main
- serve_loop<S> replaces agent_serve_loop + manager_serve_loop
- handle_turn<S> replaces handle_agent_turn + handle_manager_turn
- wake<S> replaces agent_wake + manager_wake
RecvOutcome enum decouples the per-role Response shape from the loop's
match arms so serve_loop never sees either enum.
main's dispatch picks the type parameter from HIVE_ROLE; everything
downstream is identical by construction.
net: -62 LOC vs main even with the new manager notify-on-failure +
continue-sentinel features kept.
three shared helpers replace the duplicated pre-#598 patterns:
- `log_system_event` lifts the HelperEvent parse + bus emit out of
handle_manager_turn so agents log QuestionAnswered/ContainerCrash/
reparent notifications the same way (#692 part 1).
- `format_turn_failure` produces the failure-notification body using
identity::qualified_label() instead of a label param threaded through
three layers. drops `label` from handle_agent_turn, agent_serve_loop,
agent_check_and_inject_continue.
- `consume_continue_sentinel` lifts the file-probe so both surfaces
reuse it (#692 part 2 — sentinel now works for manager too).
agent_notify_manager_of_failure → agent_notify_parent_of_failure: routes
via the <parent> sentinel landed in #703 instead of the literal string
'manager'. mirrored on manager side; root-manager failures resolve to
operator via topology::resolve_recipient.
handle_*_turn signatures now identical modulo the wire-type prefix
(part 3 acceptance from the issue).
The mara: 'many agents stuck at needs login? seem to do turns fine, no
needs login on agent page. dash shows needs login tho'.
Diagnosed: hive-ag3nt::events::Bus::emit_status writes
{state_dir}/hyperhive-needs-login when status flips to needs_login_idle
and only removes it when status flips to online. But the boot flow only
emits 'online' if the harness was previously parked in wait_for_login —
the LoginState::Online branch went straight into serve() without
touching emit_status. So a sentinel written on a prior boot (e.g. a
401-triggered park) survived a healthy re-spawn, and the dashboard's
auth_failed_sentinel(name) read it as still-needing-login forever after.
Fix: add bus.emit_status('online') at the top of the LoginState::Online
boot branch in both hive-ag3nt.rs (sub-agent) and hive-m1nd.rs (manager).
Idempotent — emit_status is a one-line write/remove on a tiny empty
file; calling it for an already-clean state is a no-op.
This addresses one half of #682. The other half (claude_has_session()
may EACCES on the agent's 0700-perm ~/.claude dir post-#658 root drop)
is c0re-side and tracked separately in the issue thread for damocles.
refs #682
CountPendingReminders and ReminderRollup were hardcoded to
MANAGER_AGENT. Both now take agent: Option<String> — None keeps the
current behavior (manager's own), Some(name) returns that agent's
reminder stats. The broker functions already take an agent name, so
this is a thin wire-protocol change. Callers (web UI stats page,
post-turn counts) pass None.
Closes#122
GetLooseEnds now takes agent: Option<String>:
- None = manager's own loose ends (default; the bug fix)
- Some("*") = hive-wide view (every approval/question/reminder)
- Some("name") = that agent's loose ends
The get_loose_ends MCP tool exposes this as an optional agent arg, so
the manager can still scan the whole swarm on demand. The web UI and
post-turn counts pass None (manager's own).
the existing ctx badge was misnamed: it summed `result.usage`, which is
the cumulative tokens billed across every inference in the turn. for
tool-heavy turns that easily exceeds the model's context window (a 600k
cached prefix × 15 sub-calls = 9M cache_read), making it useless as a
"should i compact?" signal.
now two separate badges:
ctx · N last inference's prompt size = actual context window in
use right now. parsed from each `assistant` event's
`.message.usage`; the harness tracks the most recent one
across the stream and snapshots it when the `result`
event lands.
cost · M cumulative tokens billed across the whole turn (the
previous behaviour, now correctly labelled).
both update via a single `TokenUsageChanged { ctx, cost }` SSE event at
turn-end. turn_stats grows four columns (`last_input_tokens`,
`last_output_tokens`, `last_cache_read_input_tokens`,
`last_cache_creation_input_tokens`) so the cold-load seed can paint both
badges on page load. migrations run try-and-ignore ALTERs so existing
agent dbs catch up; pre-migration rows have last-inference zeros and
yield no `ctx` seed (badge stays empty until next turn) rather than a
misleading 0.
new sqlite table at /state/hyperhive-turn-stats.sqlite on each
agent's state dir. one row per claude turn captures identity
(model, wake_from, result_kind), timing (started/ended_at,
duration_ms), cost (input/output/cache_read/cache_creation token
counts), behaviour (tool_call_count + per-tool breakdown JSON),
and post-turn snapshot metrics (open_threads_count,
open_reminders_count).
wire additions:
- AgentRequest/ManagerRequest::CountPendingReminders +
Broker::count_pending_reminders_for(agent)
- Bus::observe_stream + take_tool_calls — pumps the existing
stdout stream-json, picks out tool_use blocks, accumulates per
turn. bin loops fold the breakdown into each row.
- TurnStats::open_default + TurnStatRow + record() — best-effort
inserts; failures log + don't block the harness.
both ag3nt and m1nd bins capture started_at + duration via
Instant::elapsed, fetch open-thread + reminder counts from
hive-c0re via the existing socket (post-turn, best-effort), and
record one row at turn_end. record_kind splits ok / failed /
prompt_too_long; failures carry the error message in note.
todo entries for host-side vacuum sweep + reading the table back
into agent/dashboard badges.
new LiveEvent variants on the per-agent bus —
status_changed / model_changed / token_usage_changed /
turn_state_changed — replace the per-agent web UI's
/api/state polling for the badge row.
emit sites:
- Bus::set_model → model_changed
- Bus::record_usage → token_usage_changed
- Bus::set_state → turn_state_changed
- turn::wait_for_login → status_changed("online") on creds detect
- post_login_start / post_login_cancel → status_changed("needs_login_*")
per-agent endpoints (post_set_model / post_compact / post_new_session
/ post_cancel_turn / post_login_*) now all return 200; client
drops the post-submit refetch except on login transitions, which
still need /api/state to render the OAuth form + session stream.
client adds dispatch on the four new event kinds, threads
`currentLabel` through so the composer re-enables on a live
status flip, and no longer fires refreshState() from turn_end or
postModel — the events carry the same signal faster.
closes the per-agent half of the dashboard event-channel
refactor; TODO entry dropped.
install_configured now takes an optional notify recipient. on a
non-zero or spawn-failed 'claude plugin install', sub-agents send
the spec + stderr to manager via the hyperhive socket; manager
passes None so it doesn't message itself. boot still proceeds either
way — notification is best-effort.
run_claude now keeps a 20-line stderr ring buffer and bails with it
inline (was just 'exit <status>'). agent serve loop, on Failed (not
PromptTooLong — that's already absorbed by drive_turn's compaction
retry), sends the error body to manager via the normal hyperhive
send. swallows transport errors — failure is already in journald
and the events sqlite. manager-only harness (hive-m1nd) is unchanged
so it doesn't try to notify itself.
new hyperhive.claudePlugins NixOS option (list of strings) rendered
to /etc/hyperhive/claude-plugins.json. both hive-ag3nt and hive-m1nd
shell out 'claude plugin install <spec>' for each entry once at
startup before the turn loop opens. failures log a warning but don't
abort boot.