Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
31 changed files with 213 additions and 899 deletions

View file

@ -32,7 +32,7 @@ hand-maintained per-file tree drifts out of sync with the code.
user). `src/main.rs` is the `hive-c0re` binary — **daemon-only** user). `src/main.rs` is the `hive-c0re` binary — **daemon-only**
(`serve` + the periodic vacuum/sweep loops); the operator CLI lives in (`serve` + the periodic vacuum/sweep loops); the operator CLI lives in
the separate `hivectl` crate, which talks to the daemon over the host the separate `hivectl` crate, which talks to the daemon over the host
admin socket. Owns the sqlite broker, approval + question + reminder + admin socket. Owns the sqlite broker, approval + reminder +
schedule queues, the meta flake, lifecycle (`nixos-container` schedule queues, the meta flake, lifecycle (`nixos-container`
shellouts), gateway / forge / matrix provisioning, per-container stats, shellouts), gateway / forge / matrix provisioning, per-container stats,
and the axum operator dashboard (`dashboard.rs`). Largest crate. and the axum operator dashboard (`dashboard.rs`). Largest crate.
@ -120,7 +120,7 @@ hand-maintained per-file tree drifts out of sync with the code.
`forge_notify` are the built-in ones, but any user-configured MCP server `forge_notify` are the built-in ones, but any user-configured MCP server
can push todos here too, nothing restricts the `subsystem` set. Carries can push todos here too, nothing restricts the `subsystem` set. Carries
the loose-ends-v2 **todo** ops plus the loose-ends-v2 **todo** ops plus
harness-local reminders + questions. ⚠️ Distinct from harness-local reminders. ⚠️ Distinct from
`hive-core-agent-sock` above: **this socket never leaves the container** `hive-core-agent-sock` above: **this socket never leaves the container**
and `hive-c0re` is not in the path at all — no broker round-trip, no and `hive-c0re` is not in the path at all — no broker round-trip, no
long-poll, no marker files. long-poll, no marker files.

View file

@ -11,8 +11,7 @@ durable identity, and stop them from eating production. hyperhive is
the substrate. the substrate.
- identity = unix socket - identity = unix socket
- communication = sqlite-backed broker (`send` / `recv` / `ask` / - communication = sqlite-backed broker (`send` / `recv` / `remind`)
`answer` / `remind`)
- config = git (manager proposes, operator approves, deploys land as - config = git (manager proposes, operator approves, deploys land as
tagged commits) tagged commits)
- blast radius = container - blast radius = container

View file

@ -94,7 +94,7 @@ umount-old / mount-new / restart-cascade step.
| `request_init_config` (spawn a new child) | any agent, child added under self | | `request_init_config` (spawn a new child) | any agent, child added under self |
| config change via forge PR (any descendant's config) | any ancestor | | config change via forge PR (any descendant's config) | any ancestor |
| `get_logs` (any descendant) | any ancestor | | `get_logs` (any descendant) | any ancestor |
| moderate questions / reminders (cancel any open thread of a descendant) | any ancestor | | moderate reminders (cancel any open thread of a descendant) | any ancestor |
| `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else | | `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else |
| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | | `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) |
@ -149,16 +149,17 @@ other agents don't:
- **Scattered ownership checks** — a handful of independent - **Scattered ownership checks** — a handful of independent
manager-only overrides exist across `hive-c0re` today: loose-ends manager-only overrides exist across `hive-c0re` today: loose-ends
visibility (manager sees hive-wide, sub-agents only their own), visibility (manager sees hive-wide, sub-agents only their own),
"manager can cancel any question/reminder" overrides on the owner `destroy` refusing to act on the manager, and crash-watch skipping
check, `destroy` refusing to act on the manager, and crash-watch the manager (it auto-restarts via systemd instead of going through
skipping the manager (it auto-restarts via systemd instead of going the crash-watch loop). Each is planned to become an
through the crash-watch loop). Each is planned to become an
ancestor/descendant check instead of a manager-name check — see the ancestor/descendant check instead of a manager-name check — see the
module docs for `loose_ends.rs`, `stores/operator_questions.rs`, module docs for `loose_ends.rs`, `stores/broker.rs`, `actions.rs`,
`stores/broker.rs`, `actions.rs`, and `workers/crash_watch.rs` for and `workers/crash_watch.rs` for the current owner-check logic in
the current owner-check logic in each. (Reminder cancellation has each. (Question/answer routing and its own manager-override cancel
since moved fully in-agent — see the note on path — formerly `hive-c0re/src/questions.rs` and
`CancelLooseEndKind::Reminder` in `hive-c0re/src/questions.rs`.) `stores/operator_questions.rs` — has been removed entirely; reminder
cancellation is now handled fully in-agent, see the note on
`CancelLooseEndKind::Reminder` in `hive-c0re/src/socket_server/mod.rs`.)
None of the above is a stable interface — treat the module doc None of the above is a stable interface — treat the module doc
comments as the source of truth for exactly which checks exist today. comments as the source of truth for exactly which checks exist today.

View file

@ -598,12 +598,6 @@ renders only for the root agent. The root agent's approval-gating
behaviour comes from its CLAUDE.md / agent-specific instructions, not behaviour comes from its CLAUDE.md / agent-specific instructions, not
the system prompt template. the system prompt template.
Any agent (root or not) can also ask a structured question of the
operator or a peer agent via the `ask`/`answer` MCP tools, independent
of the approval flow above — see
`docs/conventions.md#question-routing-ask--answer` for the routing
rules, `ttl_seconds` expiry, and cancellation.
## Helper events to the submitting agent ## Helper events to the submitting agent
`Coordinator::notify_submitter(approval_id, &HelperEvent)` routes the `Coordinator::notify_submitter(approval_id, &HelperEvent)` routes the
@ -635,17 +629,6 @@ root agent. Variants (`hive_sh4re::manager::HelperEvent`):
- `NeedsUpdate { agent }` — sub-agent's recorded flake rev is - `NeedsUpdate { agent }` — sub-agent's recorded flake rev is
stale. The root agent calls `update(name)` to rebuild — idempotent, stale. The root agent calls `update(name)` to rebuild — idempotent,
no approval required. no approval required.
- `QuestionAnswered { id, question, answer, answerer }` — peer
`Answer` request (answerer = agent name) or ttl watchdog expiry
(answerer = `"ttl-watchdog"`, answer = `"[expired]"`). The dashboard's
own `/answer-question/{id}` trigger (answerer = `"operator"`) is
gone — removed along with the rest of the dashboard's ask/answer
surface, so an operator-targeted question now has no path to an
answer at all.
- `QuestionAsked { id, asker, question, options, multi }` —
fired when an agent calls `Ask { to: Some(<this-agent>), ... }`.
The recipient responds via `Answer { id, answer }` and the
asker sees the matching `QuestionAnswered`.
The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`, The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`,
`Destroyed`, `NeedsLogin`, `LoggedIn`, `ConfigReady` — are "FYI, check `Destroyed`, `NeedsLogin`, `LoggedIn`, `ConfigReady` — are "FYI, check
@ -668,11 +651,11 @@ or the dashboard `↻ R3BU1LD` button when the lock didn't move). When set,
bootstrap container yields the exact tree that was referenced. bootstrap container yields the exact tree that was referenced.
To add a new lifecycle notice: if it needs to drive an immediate turn To add a new lifecycle notice: if it needs to drive an immediate turn
(a live conversation like `Ask`/`Answer`, or something genuinely (something genuinely urgent, like `ContainerCrash`), add a
urgent), add a `HelperEvent` variant + call sites + update `HelperEvent` variant + call sites + update `prompts/system.md`'s
`prompts/system.md`'s message-event list. If it's "FYI, check when message-event list. If it's "FYI, check when convenient," call
convenient," call `push_todo`/`push_todo_submitter` directly instead — `push_todo`/`push_todo_submitter` directly instead — no new wire type
no new wire type needed. needed.
## Auto-update on startup ## Auto-update on startup

View file

@ -171,46 +171,6 @@ cutoff so a later `AckTurn` doesn't double-update and a stale
redelivery tag can't outlive its row. The operator-side sibling is redelivery tag can't outlive its row. The operator-side sibling is
the dashboard's "mark all read" (unbounded, per-agent). the dashboard's "mark all read" (unbounded, per-agent).
### Question routing (Ask / Answer)
`AgentRequest::Ask` (and the manager-flavour mirror) surfaces a
structured question that either lands in the operator-question queue
or in a peer agent's inbox. The recipient is the `to` field:
- `to = None` or `to = Some("operator")` — routes to the
operator-question queue. ⚠️ The dashboard's whole ask/answer
surface (the UI that used to render this queue and let the operator
reply) has been removed — an operator-targeted question currently
has no reader anywhere; this routing case is being removed too as
the same effort continues. The legacy `AskOperator` variant
collapses into this case.
- `to = Some(<agent>)` — peer Q&A. The target agent receives a
`HelperEvent::QuestionAsked { id, asker, question, options, multi }`
in their inbox. They reply via `AgentRequest::Answer` (or
`ManagerRequest::Answer` if they're the manager); the answer
threads back to the asker as a `HelperEvent::QuestionAnswered`
event.
Shape fields are uniform across both targets:
- `options` is advisory — a list is never enforced; peer-agent
recipients see it in their `QuestionAsked` event and can return any
string regardless.
- `multi = true` is a hint that the answerer should treat `options` as
a multi-select rather than pick-one. The answer comes back as a
single string with selections joined by `", "`.
- `ttl_seconds` auto-cancels with answer `[expired]` (and `answerer:
"ttl-watchdog"`) when the wait becomes moot. `None` = wait
indefinitely or until manual cancel.
Response shape is always `QuestionQueued { id }` — the asker stores
the id and correlates the asynchronous answer event when it lands.
Authorisation on `Answer`: only the question's `target` agent, or the
operator, is permitted to reply; an answer attempt from anyone else
fails the wire-side check. (The operator's only path to invoke this
was the now-removed dashboard — see the note above; the
authorisation rule itself is unchanged.)
### Loose-ends wire shape ### Loose-ends wire shape
`LooseEnd` is the per-row response shape for `GetLooseEnds` (both `LooseEnd` is the per-row response shape for `GetLooseEnds` (both
@ -220,15 +180,12 @@ privileged bot, etc.) can land later without breaking existing
handlers. Each row carries enough context that the caller renders handlers. Each row carries enough context that the caller renders
it directly as a bulleted list, no follow-up fetch needed. it directly as a bulleted list, no follow-up fetch needed.
Per-flavour scoping is uniform across the three variants: Per-flavour scoping is uniform across the two cancellable variants:
- **agent-flavour** `GetLooseEnds` only surfaces rows the calling - **agent-flavour** `GetLooseEnds` only surfaces rows the calling
agent has standing in. `Approval` rows only appear when the agent has standing in. `Approval` rows only appear when the
calling agent is the manager (sub-agents don't submit calling agent is the manager (sub-agents don't submit
approvals). `Question` rows surface where the agent is `asker` approvals). `Reminder` rows are scoped to `owner == self`.
OR `target` (the routing semantics from the Ask/Answer
subsection above). `Reminder` rows are scoped to `owner ==
self`.
- **manager-flavour** `GetLooseEnds` lists every pending row in - **manager-flavour** `GetLooseEnds` lists every pending row in
the swarm — full audit view. the swarm — full audit view.
@ -240,9 +197,6 @@ Per-variant fields:
blurb shown on the dashboard card. `commit_ref` is the blurb shown on the dashboard card. `commit_ref` is the
kind-specific payload (see `docs/approvals.md::Approval kinds kind-specific payload (see `docs/approvals.md::Approval kinds
(wire shapes)`). (wire shapes)`).
- `Question { id, asker, target?, question, age_seconds }` —
`target = None` = operator-routed (dashboard); `Some(agent)` =
peer-to-peer thread.
- `Reminder { id, owner, message, due_at, age_seconds }` — - `Reminder { id, owner, message, due_at, age_seconds }` —
`due_at` is the absolute time the scheduler is targeting (RFC `due_at` is the absolute time the scheduler is targeting (RFC
3339 on the wire, see *Timestamps on the wire* below); clients 3339 on the wire, see *Timestamps on the wire* below); clients
@ -265,10 +219,9 @@ unsynchronised wall clock, etc.) so the bulleted list never
shows nonsense ages. shows nonsense ages.
`CancelLooseEnd { kind, id }` is the matching write surface. The `CancelLooseEnd { kind, id }` is the matching write surface. The
`kind` enum (`Question` / `Reminder` / `Approval`) selects which `kind` enum (`Reminder` / `Approval`) selects which underlying store
underlying store the dispatcher reaches into. `Question` and the dispatcher reaches into. `Reminder` cancels from either surface
`Reminder` cancel from either surface subject to ownership subject to an ownership check (the scheduling agent).
checks (asker for the question, scheduler for the reminder).
`Approval` is manager-only — sub-agents don't submit approvals `Approval` is manager-only — sub-agents don't submit approvals
so they have nothing of their own to withdraw; their wire so they have nothing of their own to withdraw; their wire
surface returns a clear error if they try. Cancelling an approval surface returns a clear error if they try. Cancelling an approval

View file

@ -20,8 +20,8 @@ keeps its state, purging it doesn't.**
Beyond that: Beyond that:
- **Approvals and questions are kept forever** — they're audit trails, - **Approvals are kept forever** — they're an audit trail, not a
not caches. Nothing about them ever ages out. cache. Nothing about them ever ages out.
- **Broker messages**: acked ones vacuum after 30 days; anything - **Broker messages**: acked ones vacuum after 30 days; anything
undelivered or delivered-but-not-yet-acked is always kept, however undelivered or delivered-but-not-yet-acked is always kept, however
old. old.
@ -55,20 +55,11 @@ power-intent registry:
⚠️ The `mcp__hyperhive__remind` queue is **not** here any more: it ⚠️ The `mcp__hyperhive__remind` queue is **not** here any more: it
moved to a harness-local, per-agent store as part of the moved to a harness-local, per-agent store as part of the
loose-ends-v2 migration — see [`/harness/` contents loose-ends-v2 migration — see [`/harness/` contents
below](#state-dirs-per-agent) for where reminders (and todos, and below](#state-dirs-per-agent) for where reminders (and todos)
the questions mirror) actually live now. actually live now.
- `approvals` — the queue. `agent / kind (merge_config_pr | spawn | - `approvals` — the queue. `agent / kind (merge_config_pr | spawn |
init_config | update_meta_inputs | schedule_prompt) / init_config | update_meta_inputs | schedule_prompt) /
commit_ref / requested_at / status / resolved_at / note`. commit_ref / requested_at / status / resolved_at / note`.
- `operator_questions` — `ask` / `answer` queue (despite the
table name, stores both operator-targeted + agent-to-agent
questions since the `ask` rename).
`asker / question / options_json / multi / asked_at /
deadline_at (ttl) / answered_at / answer / target`. `target IS
NULL` = operator path (dashboard); `target = '<agent>'` = peer
Q&A (`HelperEvent::QuestionAsked` pushed into target's inbox,
answered via `Answer` request). Migrated via `ALTER TABLE ADD
COLUMN` against `pragma_table_info`.
- `scheduled_prompts` — recurring + one-shot prompt queue. - `scheduled_prompts` — recurring + one-shot prompt queue.
`owner / body / interval_seconds (NULL = one-shot) / `owner / body / interval_seconds (NULL = one-shot) /
next_fire_at_unix / created_at_unix / source ("operator" or next_fire_at_unix / created_at_unix / source ("operator" or
@ -98,9 +89,8 @@ Retention:
rows are always kept — the harness `ack_turn`s only after a rows are always kept — the harness `ack_turn`s only after a
successful turn, so an unacked row can still be requeued via successful turn, so an unacked row can still be requeued via
`requeue_inflight` on a crash. `requeue_inflight` on a crash.
- Approvals and questions are kept indefinitely — both are - Approvals are kept indefinitely — an audit trail. `actions::destroy`
audit trails. `actions::destroy` and answered questions stay rows stay visible to anything that queries by id.
visible to anything that queries by id.
- Scheduled prompts: one-shot rows are deleted on fire by the - Scheduled prompts: one-shot rows are deleted on fire by the
worker; recurring rows live until the operator cancels them worker; recurring rows live until the operator cancels them
(`cancel_schedule` MCP / dashboard ✗) which tombstones via (`cancel_schedule` MCP / dashboard ✗) which tombstones via
@ -301,9 +291,9 @@ Under `/var/lib/hyperhive/agents/<name>/`:
hours; non-terminal (still-running) tasks are never deleted. This hours; non-terminal (still-running) tasks are never deleted. This
used to be a host-side `hive-c0re` vacuum, moved in-container for used to be a host-side `hive-c0re` vacuum, moved in-container for
the same privsep-ownership reason as the events vacuum above. the same privsep-ownership reason as the events vacuum above.
- `hyperhive-state.sqlite` — consolidated loose-ends-v2 store: todos, - `hyperhive-state.sqlite` — consolidated loose-ends-v2 store: todos
reminders, and a questions mirror, one small table each in a single and reminders, one small table each in a single file (in-container
file (in-container daemons — `hive-bash-daemon`, `hive-matrix-daemon`, daemons — `hive-bash-daemon`, `hive-matrix-daemon`,
`hive-forge-notify` — upsert keyed todos here over the harness's `hive-forge-notify` — upsert keyed todos here over the harness's
in-agent socket, `HIVE_AGENT_SOCKET`; the harness merges them into in-agent socket, `HIVE_AGENT_SOCKET`; the harness merges them into
`get_loose_ends` output and clears a row on `mark_todo_done`). `get_loose_ends` output and clears a row on `mark_todo_done`).

View file

@ -180,8 +180,6 @@ tools (Read, Write, etc.) keep their name as-is.
| **Core hyperhive** | | | **Core hyperhive** | |
| `send*` | rich renderer: `send* → to · NL` (default-open body) | | `send*` | rich renderer: `send* → to · NL` (default-open body) |
| `recv*` | `recv*()` · `recv* wait Ns` · `recv* max N` | | `recv*` | `recv*()` · `recv* wait Ns` · `recv* max N` |
| `ask*` | rich renderer: `ask* → to` (no inline answer form — see [Inline ask-operator answer](#inline-ask-operator-answer)) |
| `answer*` | rich renderer: `answer* #id` |
| `remind*` | `remind* +Xm "preview"` or `remind* at HH:MMZ "preview"` | | `remind*` | `remind* +Xm "preview"` or `remind* at HH:MMZ "preview"` |
| `set_status*` | `set_status* "text"` | | `set_status*` | `set_status* "text"` |
| `get_loose_ends*` | `get_loose_ends*()` or `get_loose_ends* [agent]` | | `get_loose_ends*` | `get_loose_ends*()` or `get_loose_ends* [agent]` |
@ -242,15 +240,6 @@ This keeps less-frequent tools that don't have a specific
`fmt_tool_use` case from dumping raw JSON. Common matrix and `fmt_tool_use` case from dumping raw JSON. Common matrix and
hyperhive tools have their own cases and skip this path. hyperhive tools have their own cases and skip this path.
## Inline ask-operator answer
An `mcp__hyperhive__ask(to: "operator", ...)` row has no inline
answer form in this terminal — it renders like any other tool call.
The dashboard's own question-surfacing UI that used to let the
operator answer from the Y3R C4LL tab is gone (hyperhive#3721); there
is currently no UI path to answer an operator-targeted `ask()` at
all.
## Dashboard side (not covered here) ## Dashboard side (not covered here)
The main dashboard's message-flow pane is a different The main dashboard's message-flow pane is a different

View file

@ -23,8 +23,7 @@ preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and
## Core tools (always available) ## Core tools (always available)
**Messaging** (`messaging` group): `send(to, body, in_reply_to?)`, **Messaging** (`messaging` group): `send(to, body, in_reply_to?)`,
`recv(max?)`, `ask(question, options?, multi?, `recv(max?)`, `ack_until(up_to)`.
ttl_seconds?, to?)`, `answer(id, answer)`, `ack_until(up_to)`.
- `send` — message a peer (logical name) or the operator - `send` — message a peer (logical name) or the operator
(`to: "operator"`). Use `to: "<parent>"` to address the topology (`to: "operator"`). Use `to: "<parent>"` to address the topology
@ -48,13 +47,6 @@ ttl_seconds?, to?)`, `answer(id, answer)`, `ack_until(up_to)`.
other turn: if the context crossed the watermark the harness runs a other turn: if the context crossed the watermark the harness runs a
notes-checkpoint turn and then `/compact`. Compacting before shutdown notes-checkpoint turn and then `/compact`. Compacting before shutdown
keeps a later cold start cheap instead of re-uploading a huge transcript. keeps a later cold start cheap instead of re-uploading a huge transcript.
- `ask` — surface a structured question to the operator (default) or
a peer agent (`to: "<agent>"`). Non-blocking — returns a question
id; the answer arrives as a `question_answered` system event in the
asker's inbox. `options` is advisory; `multi=true` renders as
checkboxes; `ttl_seconds` auto-cancels with answer `[expired]`.
- `answer` — respond to a `question_asked` event routed to this
agent. Strict authorisation: only the declared target can answer.
- `ack_until(up_to)` — bulk-mark inbox rows handled: every row with - `ack_until(up_to)` — bulk-mark inbox rows handled: every row with
broker id `<= up_to` is stamped as acked in a single UPDATE. broker id `<= up_to` is stamped as acked in a single UPDATE.
Recipient-scoped (agents can only ack their own rows). Use when a Recipient-scoped (agents can only ack their own rows). Use when a
@ -64,14 +56,12 @@ ttl_seconds?, to?)`, `answer(id, answer)`, `ack_until(up_to)`.
Transient pings (sentinel id 0) have nothing to ack and show no marker. Transient pings (sentinel id 0) have nothing to ack and show no marker.
**System messages** (from sender `system`): the higher-urgency **System messages** (from sender `system`): the higher-urgency
lifecycle + Q&A events (`hive_sh4re::manager::HelperEvent`) are lifecycle events (`hive_sh4re::manager::HelperEvent`) are
delivered as regular inbox messages (same `recv` path; body is a JSON delivered as regular inbox messages (same `recv` path; body is a JSON
object with an `event` discriminant field). The **submitting agent** object with an `event` discriminant field). The **submitting agent**
(the root agent for top-level containers; an agent with the `approvals` (the root agent for top-level containers; an agent with the `approvals`
tool group for its own subtree) receives `container_crash`, tool group for its own subtree) receives `container_crash`,
`needs_update`, and `approval_resolved` this way. Any agent receives `needs_update`, and `approval_resolved` this way. The remaining, lower-urgency lifecycle
Q&A events when it is the declared target (`question_asked`) or the
asker (`question_answered`). The remaining, lower-urgency lifecycle
notices — `spawned`, `rebuilt`, `killed`, `destroyed`, `needs_login`, notices — `spawned`, `rebuilt`, `killed`, `destroyed`, `needs_login`,
`logged_in`, `config_ready` — skip the inbox entirely: they land as `logged_in`, `config_ready` — skip the inbox entirely: they land as
todos on the submitting agent's in-container todo socket instead todos on the submitting agent's in-container todo socket instead
@ -86,18 +76,19 @@ payload shapes and routing logic in
`cancel_loose_end(kind, id)`, `remind(message, delay_seconds? | `cancel_loose_end(kind, id)`, `remind(message, delay_seconds? |
at_unix_timestamp?)`. at_unix_timestamp?)`.
- `get_loose_ends(agent?)` — list pending questions (asked/owed), - `get_loose_ends(agent?)` — list scheduled reminders, pending
scheduled reminders, and active local tasks published by external MCP approvals you submitted, and active local tasks published by
daemons (e.g. running bash tasks from `hive-bash-daemon`). Each row external MCP daemons (e.g. running bash tasks from
carries an id + kind for `cancel_loose_end`. Omit `agent` to list `hive-bash-daemon`). Each row carries an id + kind for
your own threads. Pass `agent: "<name>"` to inspect a direct child `cancel_loose_end`. Omit `agent` to list your own threads. Pass
agent (always accessible per topology enforcement); non-children `agent: "<name>"` to inspect a direct child agent (always accessible
require the `query_agent_state` capability. The `"*"` hive-wide per topology enforcement); non-children require the
query is not available on the agent socket. `query_agent_state` capability. The `"*"` hive-wide query is not
- `cancel_loose_end` — withdraw a `question` (posts `[cancelled by available on the agent socket.
<self>]`), hard-delete a `reminder`, or cancel a pending `approval` - `cancel_loose_end` — hard-delete a `reminder`, cancel a pending
row. Agents may only cancel rows they own; the `approval` kind is `approval` row, or clear a `todo` row (loose-ends-v2). Agents may
further restricted to the root agent (`ruth`) server-side. only cancel rows they own; the `approval` kind is further restricted
to the root agent (`ruth`) server-side.
- `remind` — schedule a reminder in this agent's own inbox. Large - `remind` — schedule a reminder in this agent's own inbox. Large
payloads spill to `/agents/<self>/state/reminders/`. Pending count payloads spill to `/agents/<self>/state/reminders/`. Pending count
capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`). capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`).

View file

@ -170,14 +170,11 @@ from the harness-local store (same effect as `cancel_loose_end(kind:
"todo")`, just from the web UI instead of the agent's own tool calls). "todo")`, just from the web UI instead of the agent's own tool calls).
The todos flyout is the only per-agent flyout — there is no separate The todos flyout is the only per-agent flyout — there is no separate
"loose-ends" or "tasks" list. There is also no inline answer form for "loose-ends" or "tasks" list. There used to be a note here about the
`ask` tool calls in this terminal: an `ask` renders like any other `ask`/`answer` MCP tools having no inline answer form in this
tool call (see terminal — that whole mechanism (the tools, the dashboard's questions
[`terminal-rendering.md`](../terminal-rendering.md#inline-ask-operator-answer)). pane, the wire protocol) has since been removed entirely, so there's
The dashboard's own Y3R C4LL questions pane that used to let the nothing left to render a form for.
operator answer from there is gone (hyperhive#3721) — an
operator-targeted `ask()` currently has no UI path to an answer at
all until the removal finishes or a replacement lands.
## Live view ## Live view

View file

@ -9,7 +9,7 @@ would hit. HTTP is the sole transport; there is no stdio mode here.
## When to use it ## When to use it
This is where the core hyperhive tool surface lives: `send`, `recv`, This is where the core hyperhive tool surface lives: `send`, `recv`,
`ask`/`answer`, `remind`, `get_loose_ends`, `set_status`, `remind`, `get_loose_ends`, `set_status`,
`get_agent_meta`, lifecycle (`kill`/`start`/`restart`/`update` on `get_agent_meta`, lifecycle (`kill`/`start`/`restart`/`update` on
direct children), scheduling, and the approval-request tools. Reach direct children), scheduling, and the approval-request tools. Reach
for this crate when you're adding or changing a built-in tool rather for this crate when you're adding or changing a built-in tool rather
@ -25,7 +25,7 @@ or their own daemon instead of living here.
in-agent socket the `hive-agent` harness serves. in-agent socket the `hive-agent` harness serves.
- **`client.rs`** — socket client to the hyperhive broker. - **`client.rs`** — socket client to the hyperhive broker.
- **`send_allow.rs`** — enforces the per-agent - **`send_allow.rs`** — enforces the per-agent
`hyperhive.allowedRecipients` allow-list on `send`/`ask`. `hyperhive.allowedRecipients` allow-list on `send`.
- **`paths.rs`** — socket + state path resolution shared with the - **`paths.rs`** — socket + state path resolution shared with the
harness's own `paths.rs` conventions. harness's own `paths.rs` conventions.

View file

@ -159,14 +159,13 @@ pub struct UpdateArgs {
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CancelLooseEndArgs { pub struct CancelLooseEndArgs {
/// Which kind of thread to cancel — `"question"` for an open /// Which kind of thread to cancel — `"reminder"` for a scheduled
/// `ask` that's still waiting on an answer, `"reminder"` for a /// `remind` that hasn't fired yet, `"approval"` for a pending
/// scheduled `remind` that hasn't fired yet, or `"todo"` for a /// approval you submitted, or `"todo"` for a loose-ends-v2 todo
/// loose-ends-v2 todo (bash/matrix/forge). Use the `kind` /// (bash/matrix/forge). Use the `kind` field straight off the
/// field straight off the `get_loose_ends` row. /// `get_loose_ends` row.
pub kind: String, pub kind: String,
/// Row id from the matching `get_loose_ends` entry (or the /// Row id from the matching `get_loose_ends` entry.
/// `question_queued` reply when you submitted it).
pub id: i64, pub id: i64,
} }

View file

@ -152,7 +152,7 @@ fn msg_id_tag(id: i64) -> String {
} }
/// Hard cap on how many individual `Todo` lines `render_loose_ends` will /// Hard cap on how many individual `Todo` lines `render_loose_ends` will
/// emit. Approvals/questions/reminders stay naturally bounded (they're /// emit. Approvals/reminders stay naturally bounded (they're
/// triaged interactively and don't self-multiply), but todos are pushed by /// triaged interactively and don't self-multiply), but todos are pushed by
/// unattended producers — matrix/bash/forge are the built-in ones, but any /// unattended producers — matrix/bash/forge are the built-in ones, but any
/// user-configured MCP server can push its own too — an agent that goes a long /// user-configured MCP server can push its own too — an agent that goes a long
@ -194,19 +194,6 @@ fn render_one_loose_end(out: &mut String, t: &hive_sh4re::inbox::LooseEnd) {
"- approval #{id} ({agent} @ {commit_ref}, {age_seconds}s old){desc}" "- approval #{id} ({agent} @ {commit_ref}, {age_seconds}s old){desc}"
); );
} }
hive_sh4re::inbox::LooseEnd::Question {
id,
asker,
target,
question,
age_seconds,
} => {
let to = target.as_deref().unwrap_or("operator");
let _ = writeln!(
out,
"- question #{id} ({asker} → {to}, {age_seconds}s old): {question}"
);
}
hive_sh4re::inbox::LooseEnd::Reminder { hive_sh4re::inbox::LooseEnd::Reminder {
id, id,
owner, owner,
@ -459,23 +446,20 @@ pub(super) fn parse_loose_end_kind(
raw: &str, raw: &str,
) -> Result<hive_sh4re::inbox::CancelLooseEndKind, String> { ) -> Result<hive_sh4re::inbox::CancelLooseEndKind, String> {
match raw.trim().to_ascii_lowercase().as_str() { match raw.trim().to_ascii_lowercase().as_str() {
"question" | "q" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Question),
"reminder" | "r" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Reminder), "reminder" | "r" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Reminder),
"approval" | "a" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Approval), "approval" | "a" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Approval),
other => Err(format!( other => Err(format!(
"cancel_loose_end: unknown kind '{other}' \ "cancel_loose_end: unknown kind '{other}' \
(expected \"question\", \"reminder\", \"approval\", or \"todo\")" (expected \"reminder\", \"approval\", or \"todo\")"
)), )),
} }
} }
/// Canonical user-facing label for a `CancelLooseEndKind` — used in /// Canonical user-facing label for a `CancelLooseEndKind` — used in
/// the success ack so the caller always sees `"question"` / /// the success ack so the caller always sees `"reminder"` instead of
/// `"reminder"` instead of whatever alias they passed in (`"q"` / /// whatever alias they passed in (`"r"`).
/// `"r"`).
pub(super) fn loose_end_kind_label(kind: hive_sh4re::inbox::CancelLooseEndKind) -> &'static str { pub(super) fn loose_end_kind_label(kind: hive_sh4re::inbox::CancelLooseEndKind) -> &'static str {
match kind { match kind {
hive_sh4re::inbox::CancelLooseEndKind::Question => "question",
hive_sh4re::inbox::CancelLooseEndKind::Reminder => "reminder", hive_sh4re::inbox::CancelLooseEndKind::Reminder => "reminder",
hive_sh4re::inbox::CancelLooseEndKind::Approval => "approval", hive_sh4re::inbox::CancelLooseEndKind::Approval => "approval",
} }

View file

@ -2,7 +2,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
Tools (hyperhive surface). Full signature + behavior for each comes from the tool's own MCP description (you already received it via the MCP tool schema) — this is just the map of what exists and which ones are gated, so you know where to look: Tools (hyperhive surface). Full signature + behavior for each comes from the tool's own MCP description (you already received it via the MCP tool schema) — this is just the map of what exists and which ones are gated, so you know where to look:
- **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__ask`, `mcp__hyperhive__answer`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. Two habits worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint); and `ask`/`answer` are async — `ask` returns immediately with a question id, the reply lands later as a `question_answered` system event, never block a turn waiting on it inline. For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at. - **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at.
- **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time. - **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time.
- **Lifecycle** (_requires `lifecycle` tool group_, direct children only, no approval needed): `restart`, `kill`, `start`, `update`, `list_containers`. - **Lifecycle** (_requires `lifecycle` tool group_, direct children only, no approval needed): `restart`, `kill`, `start`, `update`, `list_containers`.
- **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_init_config`, `request_apply_commit`, `request_update_meta_inputs`. - **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_init_config`, `request_apply_commit`, `request_update_meta_inputs`.
@ -15,11 +15,11 @@ Your config repo is mounted **read-only** at `/agents/{label}/config/` — `agen
Approval boundary: lifecycle ops on _existing_ direct children (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent (two-step: `request_init_config` + `request_apply_commit`) and _changing_ any agent's config (`request_apply_commit`) both go through the approval queue (requires `approvals` tool group). The operator only signs off on changes; you run the day-to-day. Approval boundary: lifecycle ops on _existing_ direct children (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent (two-step: `request_init_config` + `request_apply_commit`) and _changing_ any agent's config (`request_apply_commit`) both go through the approval queue (requires `approvals` tool group). The operator only signs off on changes; you run the day-to-day.
Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `container_crash`, `needs_update`, `question_asked`, `question_answered`. Use these to react to lifecycle changes: Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `container_crash`, `needs_update`. Use these to react to lifecycle changes:
- `needs_update` — agent's flake rev is stale. Call `update(name)` to rebuild — it's idempotent and doesn't need approval. - `needs_update` — agent's flake rev is stale. Call `update(name)` to rebuild — it's idempotent and doesn't need approval.
- `container_crash` — restart with `start(name)`. If it crashes again, ask the operator. - `container_crash` — restart with `start(name)`. If it crashes again, ask the operator.
- otherwise pick up answers to questions you asked. - `approval_resolved` — one of your own submitted approvals (`request_init_config`, `request_apply_commit`, `request_update_meta_inputs`, a scheduled prompt, …) was approved, denied, or failed; the body carries the resolution.
Lifecycle notices that don't need an immediate turn — a new agent spawned, its config repo seeded, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them. Lifecycle notices that don't need an immediate turn — a new agent spawned, its config repo seeded, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them.

View file

@ -104,9 +104,9 @@ async fn main() -> Result<()> {
// ---------- shared turn helpers ---------- // ---------- shared turn helpers ----------
/// Surface a `SYSTEM_SENDER` message in the live event bus + tracing /// Surface a `SYSTEM_SENDER` message in the live event bus + tracing
/// log. Both agents and the manager receive `QuestionAnswered`, /// log. Both agents and the manager receive `ContainerCrash`,
/// `ContainerCrash`, reparent notifications, and friends; the parse /// reparent notifications, and friends; the parse and log path is
/// and log path is identical. Quiet no-op when `from` isn't /// identical. Quiet no-op when `from` isn't
/// `SYSTEM_SENDER`. /// `SYSTEM_SENDER`.
fn log_system_event(bus: &Bus, from: &str, body: &str) { fn log_system_event(bus: &Bus, from: &str, body: &str) {
if from != SYSTEM_SENDER { if from != SYSTEM_SENDER {

View file

@ -255,7 +255,7 @@ shared closer
assert!(!rendered.contains("<!-- /role:")); assert!(!rendered.contains("<!-- /role:"));
// Shared tools appear. // Shared tools appear.
assert!(rendered.contains("mcp__hyperhive__recv")); assert!(rendered.contains("mcp__hyperhive__recv"));
assert!(rendered.contains("mcp__hyperhive__ask")); assert!(rendered.contains("mcp__hyperhive__send"));
} }
#[test] #[test]

View file

@ -33,7 +33,7 @@ const REMINDER_BATCH_LIMIT: u64 = 100;
/// Poll interval — matches the old c0re `POLL_INTERVAL`. /// Poll interval — matches the old c0re `POLL_INTERVAL`.
const POLL_INTERVAL: Duration = Duration::from_secs(5); const POLL_INTERVAL: Duration = Duration::from_secs(5);
/// Same cap the broker used to enforce on `send`/`ask`/`remind` bodies /// Same cap the broker enforces on `send`/`remind` bodies
/// (`hive-c0re`'s `agent_config::limits::MESSAGE_MAX_BYTES`, not /// (`hive-c0re`'s `agent_config::limits::MESSAGE_MAX_BYTES`, not
/// reachable from here — hive-agent doesn't depend on hive-c0re). /// reachable from here — hive-agent doesn't depend on hive-c0re).
/// Duplicated rather than shared: this is the last remaining reminder /// Duplicated rather than shared: this is the last remaining reminder

View file

@ -254,14 +254,7 @@ fn enrich_tool_use_entry(entry: &mut Value) {
/// `_category: "rich"` so the client can distinguish without re-implementing /// `_category: "rich"` so the client can distinguish without re-implementing
/// the tool name list. /// the tool name list.
fn is_rich_tool(name: &str) -> bool { fn is_rich_tool(name: &str) -> bool {
matches!( matches!(name, "Edit" | "mcp__bash__run" | "mcp__hyperhive__send")
name,
"Edit"
| "mcp__bash__run"
| "mcp__hyperhive__send"
| "mcp__hyperhive__ask"
| "mcp__hyperhive__answer"
)
} }
/// Pre-compute the expandable body for rich tool entries. /// Pre-compute the expandable body for rich tool entries.
@ -271,7 +264,7 @@ fn is_rich_tool(name: &str) -> bool {
/// - `"diff"` → `api.detailsDiff` (colour-coded `+`/`-` lines) /// - `"diff"` → `api.detailsDiff` (colour-coded `+`/`-` lines)
/// - `"plain"` → `api.details` (plain `<pre>` block) /// - `"plain"` → `api.details` (plain `<pre>` block)
/// - `"markdown"` → `api.detailsOpenMd` (markdown rendered via marked + `DOMPurify`, /// - `"markdown"` → `api.detailsOpenMd` (markdown rendered via marked + `DOMPurify`,
/// default-open; used for message-bearing tools: send, ask, answer) /// default-open; used for message-bearing tools: `send`)
/// ///
/// Returns `None` for tools that have no body at all. /// Returns `None` for tools that have no body at all.
/// ///
@ -318,9 +311,7 @@ fn rich_tool_body(name: &str, input: &Value) -> Option<(String, &'static str)> {
Some((format!("$ {cmd}"), "plain")) Some((format!("$ {cmd}"), "plain"))
} }
} }
// Message-bearing tools: body is markdown text rendered by the client. // Message-bearing tool: body is markdown text rendered by the client.
// The ask form (operator reply slot) is still mounted client-side;
// only the raw body text moves to the backend here.
"mcp__hyperhive__send" => { "mcp__hyperhive__send" => {
let body = input.get("body").and_then(Value::as_str).unwrap_or(""); let body = input.get("body").and_then(Value::as_str).unwrap_or("");
if body.is_empty() { if body.is_empty() {
@ -329,22 +320,6 @@ fn rich_tool_body(name: &str, input: &Value) -> Option<(String, &'static str)> {
Some((body.to_owned(), "markdown")) Some((body.to_owned(), "markdown"))
} }
} }
"mcp__hyperhive__ask" => {
let q = input.get("question").and_then(Value::as_str).unwrap_or("");
if q.is_empty() {
None
} else {
Some((q.to_owned(), "markdown"))
}
}
"mcp__hyperhive__answer" => {
let a = input.get("answer").and_then(Value::as_str).unwrap_or("");
if a.is_empty() {
None
} else {
Some((a.to_owned(), "markdown"))
}
}
_ => None, _ => None,
} }
} }
@ -354,8 +329,6 @@ fn tool_icon(name: &str) -> &'static str {
match name { match name {
"mcp__hyperhive__send" => "📤", "mcp__hyperhive__send" => "📤",
"mcp__hyperhive__recv" => "📥", "mcp__hyperhive__recv" => "📥",
"mcp__hyperhive__ask" => "❓",
"mcp__hyperhive__answer" => "✍️",
"mcp__hyperhive__remind" => "⏰", "mcp__hyperhive__remind" => "⏰",
"mcp__hyperhive__set_status" => "🏷️", "mcp__hyperhive__set_status" => "🏷️",
"mcp__hyperhive__get_loose_ends" => "🪢", "mcp__hyperhive__get_loose_ends" => "🪢",
@ -470,10 +443,10 @@ fn fmt_builtin_tool(name: &str, short: &str, input: &Value) -> String {
} }
} }
/// Summary for message-bearing hyperhive tools (send / ask / answer). /// Summary for `send`, the one message-bearing hyperhive tool.
/// ///
/// Format: `"{short} → {recipient}"` or `"{short} #{id}"` with ` · NL` /// Format: `"{short} → {recipient}"` with ` · NL` appended when the body
/// appended when the body spans multiple lines. /// spans multiple lines.
fn fmt_hyperhive_message_tool(name: &str, short: &str, input: &Value) -> String { fn fmt_hyperhive_message_tool(name: &str, short: &str, input: &Value) -> String {
match name { match name {
"mcp__hyperhive__send" => { "mcp__hyperhive__send" => {
@ -485,30 +458,6 @@ fn fmt_hyperhive_message_tool(name: &str, short: &str, input: &Value) -> String
format!("{short} → {to}") format!("{short} → {to}")
} }
} }
"mcp__hyperhive__ask" => {
let to = input
.get("to")
.and_then(Value::as_str)
.unwrap_or("operator");
let lines = sv(input, "question").lines().count();
if lines > 1 {
format!("{short} → {to} · {lines}L")
} else {
format!("{short} → {to}")
}
}
"mcp__hyperhive__answer" => {
let id = input
.get("id")
.and_then(Value::as_u64)
.map_or_else(|| "?".to_owned(), |n| n.to_string());
let lines = sv(input, "answer").lines().count();
if lines > 1 {
format!("{short} #{id} · {lines}L")
} else {
format!("{short} #{id}")
}
}
_ => fmt_args_generic(short, input), _ => fmt_args_generic(short, input),
} }
} }
@ -516,9 +465,7 @@ fn fmt_hyperhive_message_tool(name: &str, short: &str, input: &Value) -> String
/// `mcp__hyperhive__*` tools. /// `mcp__hyperhive__*` tools.
fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String { fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String {
match name { match name {
"mcp__hyperhive__send" | "mcp__hyperhive__ask" | "mcp__hyperhive__answer" => { "mcp__hyperhive__send" => fmt_hyperhive_message_tool(name, short, input),
fmt_hyperhive_message_tool(name, short, input)
}
"mcp__hyperhive__recv" => { "mcp__hyperhive__recv" => {
let mut parts = Vec::new(); let mut parts = Vec::new();
if let Some(w) = input.get("wait_seconds").and_then(Value::as_u64) { if let Some(w) = input.get("wait_seconds").and_then(Value::as_u64) {

View file

@ -1,7 +1,7 @@
# hive-c0re # hive-c0re
The unprivileged host daemon (runs as `hive-core`). Owns the sqlite The unprivileged host daemon (runs as `hive-core`). Owns the sqlite
broker, the approval/question/schedule queues, the generic job-DAG broker, the approval/reminder/schedule queues, the generic job-DAG
queue, container lifecycle, gateway/forge/matrix provisioning, queue, container lifecycle, gateway/forge/matrix provisioning,
per-container stats, and the axum operator dashboard. Largest crate in per-container stats, and the axum operator dashboard. Largest crate in
the workspace — bin-only, no separate lib. the workspace — bin-only, no separate lib.
@ -24,7 +24,7 @@ module's own `//!` doc-comment for real detail, don't expect this file
to track it: to track it:
- **`dashboard/`** — the operator dashboard (containers, approvals, - **`dashboard/`** — the operator dashboard (containers, approvals,
schedules, questions, logs, topology). schedules, logs, topology).
- **`job_queue/`** — the job-DAG queue + desired-state reconciliation - **`job_queue/`** — the job-DAG queue + desired-state reconciliation
(`docs/coordinator.md`). (`docs/coordinator.md`).
- **`lifecycle/`** — `nixos-container` lifecycle + per-agent config - **`lifecycle/`** — `nixos-container` lifecycle + per-agent config

View file

@ -10,8 +10,8 @@
//! about it — oversized reminder bodies get persisted to disk //! about it — oversized reminder bodies get persisted to disk
//! transparently and the inbox sees a pointer. //! transparently and the inbox sees a pointer.
/// Per-message body cap. Applies to `send`, `ask` question text, /// Per-message body cap. Applies to `send` bodies and the stored
/// `answer` body, and the stored inline form of a reminder. 4 KiB /// inline form of a reminder. 4 KiB
/// catches the bulk of conversational overflow (status reports, /// catches the bulk of conversational overflow (status reports,
/// bullet-list summaries, short proposals) while staying small /// bullet-list summaries, short proposals) while staying small
/// enough that a backed-up inbox of ~10 unread messages only adds /// enough that a backed-up inbox of ~10 unread messages only adds
@ -112,8 +112,8 @@ mod tests {
#[test] #[test]
fn label_threads_through() { fn label_threads_through() {
let err = check_size("question", &"x".repeat(MESSAGE_MAX_BYTES + 1)).unwrap_err(); let err = check_size("broadcast", &"x".repeat(MESSAGE_MAX_BYTES + 1)).unwrap_err();
assert!(err.starts_with("question body too long")); assert!(err.starts_with("broadcast body too long"));
} }
#[test] #[test]

View file

@ -15,7 +15,6 @@ use crate::broker::Broker;
use crate::container_view::{self, ContainerView}; use crate::container_view::{self, ContainerView};
use crate::dashboard_events::DashboardEvent; use crate::dashboard_events::DashboardEvent;
use crate::job_queue::RunningTransient; use crate::job_queue::RunningTransient;
use crate::operator_questions::OperatorQuestions;
use crate::socket_server::{self, AgentSocket}; use crate::socket_server::{self, AgentSocket};
/// Capacity of the dashboard event channel. Slow browser subscribers /// Capacity of the dashboard event channel. Slow browser subscribers
@ -32,7 +31,6 @@ const LAST_STOPPED_RUNNING_KEY: &str = "last_stopped_running";
pub struct Coordinator { pub struct Coordinator {
pub broker: Arc<Broker>, pub broker: Arc<Broker>,
pub approvals: Arc<Approvals>, pub approvals: Arc<Approvals>,
pub questions: Arc<OperatorQuestions>,
/// Scheduled-prompts queue. One sqlite connection, /// Scheduled-prompts queue. One sqlite connection,
/// internal mutex; the worker drains due rows and the manager /// internal mutex; the worker drains due rows and the manager
/// handlers insert / cancel through the same handle. /// handlers insert / cancel through the same handle.
@ -467,7 +465,6 @@ impl Coordinator {
} = env; } = env;
let broker = Broker::open(db_path).context("open broker")?; let broker = Broker::open(db_path).context("open broker")?;
let approvals = Approvals::open(db_path).context("open approvals")?; let approvals = Approvals::open(db_path).context("open approvals")?;
let questions = OperatorQuestions::open(db_path).context("open operator_questions")?;
let scheduled_prompts = crate::scheduled_prompts::ScheduledPrompts::open(db_path) let scheduled_prompts = crate::scheduled_prompts::ScheduledPrompts::open(db_path)
.context("open scheduled_prompts")?; .context("open scheduled_prompts")?;
// BuildLogs wants a directory (it picks its own `build_logs.sqlite` // BuildLogs wants a directory (it picks its own `build_logs.sqlite`
@ -496,7 +493,6 @@ impl Coordinator {
Ok(Self { Ok(Self {
broker: Arc::new(broker), broker: Arc::new(broker),
approvals: Arc::new(approvals), approvals: Arc::new(approvals),
questions: Arc::new(questions),
scheduled_prompts: Arc::new(scheduled_prompts), scheduled_prompts: Arc::new(scheduled_prompts),
build_logs, build_logs,
audit_log, audit_log,
@ -971,9 +967,8 @@ impl Coordinator {
std::fs::create_dir_all(&agent_dir) std::fs::create_dir_all(&agent_dir)
.with_context(|| format!("create agent dir {}", agent_dir.display()))?; .with_context(|| format!("create agent dir {}", agent_dir.display()))?;
let socket_path = Self::socket_path(name); let socket_path = Self::socket_path(name);
// Hand the full Coordinator to the per-agent socket — it // Hand the full Coordinator to the per-agent socket — it needs
// needs broker + operator_questions to handle the agent-side // more than just the broker (approvals, scheduled_prompts, ...).
// `ask` / `answer` tools, not just the broker.
let socket = socket_server::start(name, &socket_path, self.clone())?; let socket = socket_server::start(name, &socket_path, self.clone())?;
self.agents.lock().unwrap().insert(name.to_owned(), socket); self.agents.lock().unwrap().insert(name.to_owned(), socket);
Ok(agent_dir) Ok(agent_dir)
@ -1336,17 +1331,15 @@ impl Coordinator {
/// Push a `HelperEvent` into an arbitrary agent's inbox. Encoded /// Push a `HelperEvent` into an arbitrary agent's inbox. Encoded
/// the same way as `notify_manager` (sender = `SYSTEM_SENDER`, /// the same way as `notify_manager` (sender = `SYSTEM_SENDER`,
/// body = JSON-encoded event). Used to route `QuestionAnswered` /// body = JSON-encoded event) — e.g. `ContainerCrash`, `NeedsUpdate`.
/// events back to the agent that called `ask`, `QuestionAsked`
/// events to the target of a peer question, etc.
pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::manager::HelperEvent) { pub fn notify_agent(&self, agent: &str, event: &hive_sh4re::manager::HelperEvent) {
self.notify_agent_from(hive_sh4re::manager::SYSTEM_SENDER, agent, event); self.notify_agent_from(hive_sh4re::manager::SYSTEM_SENDER, agent, event);
} }
/// Same as `notify_agent` but with an explicit sender. Use this /// Same as `notify_agent` but with an explicit sender. Use this
/// when the event originates from a known agent or the operator /// when the event originates from a known agent or the operator
/// (e.g. `QuestionAnswered` — the answerer should be the `from`, /// rather than the system itself, so the recipient's terminal
/// not `system`) so the recipient's terminal shows the right name. /// shows the right name instead of `system`.
pub fn notify_agent_from( pub fn notify_agent_from(
&self, &self,
from: &str, from: &str,

Some files were not shown because too many files have changed in this diff Show more