diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index de57c8cc..4ad46ed0 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -110,19 +110,6 @@ at_unix_timestamp?)`, `request_next_turn()`. `homeserver`); omitted for agents with no matrix provisioning. Omit `name` to query self. -**Always-on, no tool group** (like `set_status`): `compact()`. - -- `compact` — agent self-service equivalent of the operator dashboard's - `/compact` button. No args. Gated server-side on the last completed - turn's context usage: refused (with an explanation, no side effect) - unless usage is above 66% of the effective context window. On a - pass, queues the same deferred `compact_pending` flag the dashboard - button sets — consumed at the end of the current turn, so it never - races a live claude process, and the usual pre-compaction - notes-checkpoint turn still fires first. Dispatched through the - in-agent socket (`hive-agent-sock::Request::Compact`), not the - broker — see `hive-agent/src/todo_server.rs`. - ## Privileged tools (by tool group) - **Bash execution** (`execution`) — background shell tasks. See diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index d3a14b91..177379c7 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -587,29 +587,6 @@ impl AgentServer { .await } - #[tool( - description = "Compact the current session's context, mirroring the operator's \ - dashboard `/compact` button. Gated: only honoured when this agent's last \ - completed turn used more than 66% of the effective context window — below \ - that the call is refused with an explanation and has no effect. On a pass, \ - queues compaction for the end of the current turn (same deferred mechanism \ - the dashboard button uses, so it never races a live claude process); the \ - usual pre-compaction notes-checkpoint turn still fires first. No args." - )] - async fn compact(&self) -> String { - run_tool_envelope("compact", String::new(), async move { - match dial_agent_socket(&hive_agent_sock::Request::Compact).await { - Some(hive_agent_sock::Response::Ok) => { - "compact queued — will run at the end of the current turn".to_owned() - } - Some(hive_agent_sock::Response::Err { message }) => message, - Some(other) => format!("compact: unexpected response: {other:?}"), - None => "compact failed: in-agent socket unavailable".to_owned(), - } - }) - .await - } - // IMPORTANT: this tool is only available when the `lifecycle` tool group // is granted to this agent. hive-c0re enforces the topology check // server-side: the call is rejected unless `name` is a direct child. diff --git a/hive-agent-sock/src/lib.rs b/hive-agent-sock/src/lib.rs index a3ff9a45..a94a5908 100644 --- a/hive-agent-sock/src/lib.rs +++ b/hive-agent-sock/src/lib.rs @@ -83,14 +83,6 @@ pub enum Request { /// reminder-activity chart data (was `hive-core-agent-sock`'s /// `ReminderRollup` against the broker; now served from the local store). ReminderRollup { since_secs: u64 }, - /// Agent self-service request to compact the current session, mirroring - /// the operator's `/compact` dashboard button. Gated server-side: only - /// honoured when the last completed turn's context usage is above 66% - /// of the effective context window — below that, `Response::Err` - /// explains why and takes no action. On a pass, queues the same - /// deferred `compact_pending` flag the operator's button sets (consumed - /// at the next turn boundary), so it never races a live claude process. - Compact, } /// A response on the in-agent socket. Serialised with a `kind` tag, diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index a5249d33..d4b546a3 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -491,9 +491,8 @@ async fn serve_main(socket: &Path, poll_ms: u64) -> Result<()> { let store = Arc::new(store); let wake = todo_wake.clone(); let reminders = reminder_store.clone(); - let bus_for_socket = bus.clone(); tokio::spawn(async move { - if let Err(e) = todo_server::run(store, wake, reminders, bus_for_socket).await { + if let Err(e) = todo_server::run(store, wake, reminders).await { tracing::error!(error = %e, "in-agent todo socket exited with error"); } }); diff --git a/hive-agent/src/todo_server.rs b/hive-agent/src/todo_server.rs index 3bc280a1..ace90cb5 100644 --- a/hive-agent/src/todo_server.rs +++ b/hive-agent/src/todo_server.rs @@ -1,18 +1,14 @@ -//! In-agent socket server (loose-ends v2 + harness-local reminders + -//! self-service compact). Binds the harness-owned `HIVE_AGENT_SOCKET` and -//! serves the `hive-agent-sock` protocol to the in-container producers -//! (matrix / bash daemons, forge-notify) and to `hive-agent-mcp`'s -//! `remind`/`get_loose_ends`/`cancel_loose_end`/`compact` tool impls. Todo -//! ops hit the harness-local [`Todos`] store; a new-or-changed upsert fires -//! an in-process [`Notify`] so the serve loop drives a turn. Reminder ops -//! hit the harness-local [`Reminders`] store (`None` when the store failed -//! to open — every reminder op then returns `Response::Err`); a reminder -//! *firing* is a separate path (`reminder_timer`), not driven through this -//! socket. `Request::Compact` is the odd one out — it doesn't touch either -//! store, just the harness's [`Bus`] (gate-checked context usage, then the -//! same deferred `compact_pending` flag the operator dashboard's -//! `/compact` button sets). No hive-c0re round-trip, no broker long-poll, -//! no marker files. +//! In-agent socket server (loose-ends v2 + harness-local reminders). Binds +//! the harness-owned `HIVE_AGENT_SOCKET` and serves the `hive-agent-sock` +//! protocol to the in-container producers (matrix / bash daemons, +//! forge-notify) and to `hive-agent-mcp`'s `remind`/`get_loose_ends`/ +//! `cancel_loose_end` tool impls. Todo ops hit the harness-local [`Todos`] +//! store; a new-or-changed upsert fires an in-process [`Notify`] so the +//! serve loop drives a turn. Reminder ops hit the harness-local +//! [`Reminders`] store (`None` when the store failed to open — every +//! reminder op then returns `Response::Err`); a reminder *firing* is a +//! separate path (`reminder_timer`), not driven through this socket. No +//! hive-c0re round-trip, no broker long-poll, no marker files. //! //! One request/response line per connection, matching the producers' //! existing best-effort JSON-line clients (they just change which socket @@ -28,17 +24,9 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; use tokio::sync::Notify; -use crate::events::Bus; use crate::reminders::{Reminder, Reminders}; use crate::todos::{Todo, Todos}; -/// Context-usage floor for `Request::Compact`: below this fraction of the -/// effective context window, an agent-initiated compact is refused (nothing -/// meaningful to reclaim yet, and it'd just burn a compaction pass on a -/// near-empty session). Same number a human operator would eyeball off the -/// dashboard's `ctx·Nk` badge before hitting the `/compact` button. -const COMPACT_MIN_USAGE_FRACTION: f64 = 0.66; - /// Resolve the in-agent socket path from `HIVE_AGENT_SOCKET`. `None` when /// unset/empty (e.g. a standalone dev run) — the server then stays off. fn socket_path() -> Option { @@ -84,7 +72,6 @@ pub async fn run( store: Arc, wake: Arc, reminders: Option>, - bus: Bus, ) -> Result<()> { let Some(path) = socket_path() else { tracing::info!("HIVE_AGENT_SOCKET unset — in-agent todo socket disabled"); @@ -98,11 +85,8 @@ pub async fn run( let store = store.clone(); let wake = wake.clone(); let reminders = reminders.clone(); - let bus = bus.clone(); tokio::spawn(async move { - if let Err(e) = - handle_conn(stream, &store, &wake, reminders.as_deref(), &bus).await - { + if let Err(e) = handle_conn(stream, &store, &wake, reminders.as_deref()).await { tracing::warn!(error = ?e, "in-agent todo connection failed"); } }); @@ -133,7 +117,6 @@ async fn handle_conn( store: &Todos, wake: &Notify, reminders: Option<&Reminders>, - bus: &Bus, ) -> Result<()> { let (read, mut write) = stream.into_split(); let mut reader = BufReader::new(read); @@ -142,7 +125,7 @@ async fn handle_conn( return Ok(()); } let resp = match serde_json::from_str::(line.trim()) { - Ok(req) => dispatch(req, store, wake, reminders, bus), + Ok(req) => dispatch(req, store, wake, reminders), Err(e) => Response::Err { message: format!("bad request: {e}"), }, @@ -157,13 +140,7 @@ async fn handle_conn( /// Apply one request to the store, firing `wake` on a new/changed upsert so /// the serve loop runs a turn. `reminders` is `None` when that store /// failed to open at boot — every reminder op then returns an `Err`. -fn dispatch( - req: Request, - store: &Todos, - wake: &Notify, - reminders: Option<&Reminders>, - bus: &Bus, -) -> Response { +fn dispatch(req: Request, store: &Todos, wake: &Notify, reminders: Option<&Reminders>) -> Response { match req { Request::UpsertTodo { subsystem, @@ -256,52 +233,9 @@ fn dispatch( } None => no_reminders_store(), }, - Request::Compact => compact(bus), } } -/// `Request::Compact` handler: gate on context usage, then queue the same -/// deferred `compact_pending` flag the operator's `/compact` button sets. -/// Mirrors `hive-agent::web_ui::actions::post_compact` but reachable from -/// the agent's own MCP tool instead of the dashboard, and refuses below -/// [`COMPACT_MIN_USAGE_FRACTION`] instead of always honouring the request — -/// an agent can call this speculatively, a human clicking the dashboard -/// button already made the judgment call. -fn compact(bus: &Bus) -> Response { - let Some(usage) = bus.last_ctx_usage() else { - return Response::Err { - message: "compact refused: no completed turn yet — nothing to compact".to_owned(), - }; - }; - let model = bus.model(); - let window = bus.effective_context_window(&model); - if window == 0 { - return Response::Err { - message: "compact refused: effective context window is unknown (0)".to_owned(), - }; - } - // Token counts stay well under 2^53 in practice, so the f64 conversion - // is exact; this is a threshold ratio, not a byte-count display, but the - // same "cosmetic precision loss" reasoning as `hivectl::quota::human_bytes` applies. - #[allow(clippy::cast_precision_loss)] - let fraction = usage.context_tokens() as f64 / window as f64; - if fraction < COMPACT_MIN_USAGE_FRACTION { - return Response::Err { - message: format!( - "compact refused: context usage is {:.0}% of the {window}-token window, \ - below the {:.0}% floor — not worth compacting yet", - fraction * 100.0, - COMPACT_MIN_USAGE_FRACTION * 100.0 - ), - }; - } - bus.request_compact(); - bus.emit(crate::events::LiveEvent::Note { - text: "agent: self-requested /compact — running at the end of the current turn".into(), - }); - Response::Ok -} - /// Shared "reminders db unavailable" response for every reminder op when /// the store failed to open at boot (see `main.rs`'s best-effort open). fn no_reminders_store() -> Response { diff --git a/hive-host-sock/Cargo.toml b/hive-host-sock/Cargo.toml index cdb83f53..b39f0f9e 100644 --- a/hive-host-sock/Cargo.toml +++ b/hive-host-sock/Cargo.toml @@ -2,7 +2,6 @@ name = "hive-host-sock" edition.workspace = true version.workspace = true -readme = "README.md" [lints] workspace = true diff --git a/hive-host-sock/README.md b/hive-host-sock/README.md deleted file mode 100644 index d9f427cc..00000000 --- a/hive-host-sock/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# hive-host-sock - -Wire types for the **host admin socket** (`/run/hyperhive/host.sock`) — the -host-control protocol spoken between the `hivectl` operator CLI and the -`hive-c0re` daemon. - -## Why it's its own crate - -Re-homed out of `hive-sh4re` so a standalone `hivectl` depends on **just this -protocol crate** instead of the whole daemon-shared crate. `hivectl` drives the -full hive (spawn / kill / destroy / rebuild / deploy) over this socket without -linking `hive-c0re`; keeping the request/response shapes here is what makes that -thin dependency possible. - -## Shape - -Serde-derived request/response enums for the host admin protocol. The larger -shared payload types some variants reference (`Approval`, `AgentStatusRow`, -`jobs::DagView`) stay in `hive-sh4re` — this crate is only the protocol -envelope, no server or client implementation. - -See `docs/boundary.md` (host admin socket access) for the trust model around who -may connect to the socket, and `hive-priv-sock` for the sibling split on the -privileged-helper socket. diff --git a/hive-jobq/Cargo.toml b/hive-jobq/Cargo.toml index aa5370f7..13059fa1 100644 --- a/hive-jobq/Cargo.toml +++ b/hive-jobq/Cargo.toml @@ -2,7 +2,6 @@ name = "hive-jobq" edition.workspace = true version.workspace = true -readme = "README.md" [lints] workspace = true diff --git a/hive-jobq/README.md b/hive-jobq/README.md deleted file mode 100644 index 46bcad53..00000000 --- a/hive-jobq/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# hive-jobq - -A persistent job-DAG scheduler, extracted from hive-c0re's in-tree `job_queue` -as a **domain-agnostic** library. It schedules a single persistent graph of -nodes over named resources; it knows nothing about containers, rebuilds, or any -hyperhive type — the node payload `N` and resource name `R` are both generic, so -the caller supplies its own domain. - -## When to use it - -Reach for this crate whenever you need to run a DAG of interdependent work items -under bounded, named concurrency — the hive-c0re rebuild/lifecycle queue is the -first consumer, but nothing here is specific to it. The caller defines the node -kinds, wires deps, and supplies a runner; the scheduler decides what can start. - -## Model - -One **persistent graph** for the whole system, not a DAG per job. Enqueuing -inserts a self-contained sub-DAG and returns the new node ids; the scheduler -runs a continuous loop, starting every node whose deps are satisfied: - -- **Resource deps** are named counting semaphores over a caller-chosen type `R` - — e.g. `build-slot` (capacity N), `agent/` (capacity 1), or any - unconfigured name (capacity 1, created on use). A node acquires *all* its - resource deps atomically at start (all-or-nothing) — no hold-and-wait, so no - deadlock. -- **Node deps** wait on another node per `DepWhen`: `AfterOk` needs success (a - failed dep cancels the dependent), `AfterAny` only needs terminal. - -A node carries two independent axes: its `Dep`s (ordering + resource needs) and -its `parent` (structural grouping). The **parent chain**, not the node edges, is -what the scheduler consults for resource re-entrancy: a resource unit is held -for the acquiring node *plus its whole parent subtree*, and a descendant needing -a resource an ancestor already holds re-uses that grant (a re-entrant borrow, -one branch at a time) rather than taking a fresh unit. - -A `NodeId` is opaque, stable, and monotonic (safe to persist). The scheduler is -single-threaded — it owns the resource table and mutates it directly. - -## Shape - -- **`Graph`** — the persistent node store. `insert` mints ids and - validates dep/parent references; `set_state` is the single state-transition - choke point (and where each node's lifecycle timestamps — - `started_at` / `finished_at`, `DateTime` — are stamped). -- **`Node`** — `{ id, parent, payload, deps, state, started_at, - finished_at, error }`. All fields public; derives serde for persistence + the - wire. -- **`Scheduler`** — drives the graph: `settle()` starts every ready node - (acquiring resources atomically), `complete(id, outcome)` reports a finished - node's result and rolls terminality up the parent chain, releasing grants once - a subtree is done. `Outcome::{Done, Failed(String)}` — the failure reason - rides `Failed` onto the node's `error`. -- **`ResourceTable`** — per-name capacities; unconfigured names default to - capacity 1. - -See the crate-root and `scheduler` module `//!` docs for the full borrow/release -model. diff --git a/hive-priv-sock/Cargo.toml b/hive-priv-sock/Cargo.toml index d147d12e..e2902d84 100644 --- a/hive-priv-sock/Cargo.toml +++ b/hive-priv-sock/Cargo.toml @@ -2,7 +2,6 @@ name = "hive-priv-sock" edition.workspace = true version.workspace = true -readme = "README.md" [lints] workspace = true diff --git a/hive-priv-sock/README.md b/hive-priv-sock/README.md deleted file mode 100644 index bbfdfbba..00000000 --- a/hive-priv-sock/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# hive-priv-sock - -Wire types for the **`hive-priv` privileged-helper socket** -(`/run/hive/priv.sock`) — the contract between `hive-priv` (the root helper, -server) and `hive-c0re` (client, via its `priv_client`). - -## Why it's its own crate - -Split out of `hive-sh4re` so `hive-priv` — a **root-privileged** binary — -depends on just this narrow protocol crate instead of the much larger -daemon-shared crate. Two wins: fewer dependencies in a root process's supply -chain, and a small, self-contained interface makes the privilege boundary this -crate encodes easier to audit. Mirrors `hive-host-sock`'s split for the host -admin socket. - -## Shape - -Serde-derived request/response types only — no server or client logic. Both -sides import them so the shapes stay in sync. See `docs/boundary.md` + -`docs/security.md` for the privilege boundary these types sit on, and -`hive-priv/README` for the helper itself. diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 08dc82b5..56dfef38 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -640,13 +640,7 @@ impl ToolGroup { /// dark on the dashboard. The server-side `SetStatus` handler has no /// tool-group check either (only length validation), so listing it here /// keeps the `--allowedTools` list honest with that reality. - /// - /// `compact` lives here too: it's pure self-management (no cross-agent - /// effect, no privilege), gated server-side on context usage rather - /// than on tool groups, and every agent should be able to reach for it - /// regardless of which optional groups it's been granted — same - /// reasoning as `set_status`. - pub const ALWAYS_ON_TOOLS: &'static [&'static str] = &["set_status", "compact"]; + pub const ALWAYS_ON_TOOLS: &'static [&'static str] = &["set_status"]; /// The Claude built-in tool names enabled by this group. Only /// `WebTools` returns a non-empty slice; all other groups return `&[]`