hive-subagent-daemon and hive-bash-daemon are now delegated their cgroup (Delegate=memory pids, DelegateSubgroup=daemon). At start each daemon enables memory+pids below its unit and creates runs/ (subagent) or tasks/ (bash). Each subagent run gets runs/<name>-<n>/ and each bash task tasks/<id>/, both with memory.oom.group=1, and the daemon removes the directory when the run ends. An OOM kill now takes one whole run instead of one process out of the daemon's shared cgroup, and never the daemon itself. Subagent runs get oom_score_adj +1000; bash tasks and both daemons stay at 0; the harness stays at -500. The unit-level OOMScoreAdjust=500 on the subagent daemon goes: it was both the daemon's own score and a floor no run below it could be lowered past. Bash tasks enter their cgroup from the existing pre_exec hook, writing to a cgroup.procs fd opened by the parent. hive-claude builds its Command itself and exposes neither a pre_exec hook nor the child's pid, so a subagent's claude (and an ACP agent's process) is started as the daemon's own binary with the run's cgroup in its environment; main() enters the cgroup, raises oom_score_adj and execs the real program, the trampoline swarm-review-classifier already uses. Both enter before the program executes, so nothing it forks starts outside the run's cgroup. Without a delegated `daemon` leaf (tests, a dev shell) each daemon logs one warning and runs everything in its own cgroup, as before. The shared helper is hive-agent-sock's run_cgroup module, the crate both daemons already use for what in-container plugins share. Closes #5007
160 lines
8 KiB
Rust
160 lines
8 KiB
Rust
//! Rescoped beyond its original name: what's common across the in-container
|
|
//! "agent plugins" (matrix/bash/subagent MCP daemons, `forge_notify`, and any
|
|
//! user-configured MCP server) — not just the in-agent socket wire types.
|
|
//!
|
|
//! **Wire types for the *in-agent* socket**, served by the hive-agent harness
|
|
//! to those producers. `subsystem` on the todo ops below is a plain string,
|
|
//! not a closed set: any user-configured MCP server declared in an agent's
|
|
//! `agent.nix` can dial this socket and push its own todos the same way.
|
|
//! Carries the loose-ends-v2 *todo* op family plus the harness-local
|
|
//! *reminder* op family; more in-agent request families may be added over
|
|
//! time (the socket is deliberately named for the agent, not the todos).
|
|
//!
|
|
//! Distinct from `hive-core-agent-sock`, the *host*-served core↔agent
|
|
//! protocol on `/run/hive/mcp.sock`: this socket never leaves the
|
|
//! container. The harness owns the todo + reminder stores locally and
|
|
//! signals its own turn loop directly, so hive-c0re is not in either path —
|
|
//! no broker round-trip, no long-poll, no marker files.
|
|
//!
|
|
//! **`extra_mcp`**: the `services.hyperhive.agent.extraMcpServers` spec + parsing, shared by
|
|
//! `hive-agent` (the main session's `--mcp-config`) and `hive-subagent-mcp`
|
|
//! (the subagent-eligible subset) — not a socket protocol at all, but this
|
|
//! crate is the natural home since both consumers already depend on it.
|
|
//!
|
|
//! **`run_cgroup`**: the per-run cgroups `hive-bash-mcp` and
|
|
//! `hive-subagent-mcp` put their tasks and subagent runs in.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use hive_sh4re::inbox::LooseEnd;
|
|
|
|
pub mod extra_mcp;
|
|
pub mod paths;
|
|
pub mod run_cgroup;
|
|
|
|
/// In-container path of the harness-served in-agent socket. The harness
|
|
/// binds it on boot; the in-container producers dial it for todo ops.
|
|
/// (Placeholder default — the harness + producers resolve the real path
|
|
/// from config; kept here so a producer with no override has a sane one.)
|
|
pub const DEFAULT_AGENT_SOCKET: &str = "/run/hive/agent.sock";
|
|
|
|
/// A request on the in-agent socket. Serialised with a `cmd` tag so the
|
|
/// in-container producers can emit a plain JSON line without linking a
|
|
/// typed client (matrix/bash build the JSON by hand; a user-configured
|
|
/// MCP server can do the same with its own `subsystem` marker).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "cmd", rename_all = "snake_case")]
|
|
pub enum Request {
|
|
/// Upsert a todo from an in-container subsystem — matrix / bash /
|
|
/// forge are the built-in producers, but any subsystem name is
|
|
/// accepted; nothing restricts `subsystem` to a fixed set, so a
|
|
/// user-configured MCP server can push its own todos here too.
|
|
/// `subsystem` is the producer marker; `key` the optional
|
|
/// subsystem-specific dedup key (a matrix room id, a bash task id).
|
|
/// A new-or-changed row signals the turn loop; an identical keyed
|
|
/// re-push is a silent no-op. Keyless todos always insert as one-offs.
|
|
///
|
|
/// `reopen_if_acked` (default `false`) governs what "changed" means
|
|
/// for an *acked* row with byte-identical `summary`/`source`: by
|
|
/// default it stays quiet — the right behavior for a reconciler
|
|
/// producer re-announcing a still-true condition (`disk_watch` polling
|
|
/// and finding the same problem). A producer whose keyed re-push
|
|
/// represents a genuinely new occurrence rather than a restatement
|
|
/// (a recurring scheduled prompt firing again with the same body) sets
|
|
/// this `true` so an acked row reopens and re-wakes instead of being
|
|
/// silently swallowed by the previous occurrence's ack.
|
|
UpsertTodo {
|
|
subsystem: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
key: Option<String>,
|
|
summary: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
source: Option<String>,
|
|
#[serde(default)]
|
|
reopen_if_acked: bool,
|
|
},
|
|
/// Clear producer-resolved todo(s). `key = Some(k)` clears the one
|
|
/// keyed row; `key = None` clears the subsystem's keyless rows; `all
|
|
/// = true` wipes the producer's whole set (cancel-and-recreate on
|
|
/// daemon restart).
|
|
ClearTodo {
|
|
subsystem: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
key: Option<String>,
|
|
#[serde(default)]
|
|
all: bool,
|
|
},
|
|
/// List todos, optionally filtered to one `subsystem` (a producer
|
|
/// reconciling its own set). `None` = all.
|
|
ListTodos {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
subsystem: Option<String>,
|
|
},
|
|
/// The agent marks one of its own todos done, by id.
|
|
MarkTodoDone { id: i64 },
|
|
/// The agent bulk-acks a specific, explicit list of its own todos in
|
|
/// one shot — for clearing a backlog that's piled up past the point of
|
|
/// per-id triage, without the range-based `ack_until`-style semantics
|
|
/// that risk silently acking something never actually looked at (see
|
|
/// `Todos::mark_done_many`'s doc for why it's ids, not a threshold).
|
|
MarkTodosDone { ids: Vec<i64> },
|
|
/// Schedule a reminder that fires into this agent's own turn loop at
|
|
/// `timing` (harness-local — no broker round-trip). Same semantics as
|
|
/// the old broker `Remind` request. `file_path`, when set, is where the
|
|
/// harness persists an over-cap body instead of inlining it.
|
|
StoreReminder {
|
|
message: String,
|
|
timing: hive_sh4re::inbox::ReminderTiming,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
file_path: Option<String>,
|
|
},
|
|
/// List this agent's pending reminders (single-agent scope — unlike
|
|
/// the old broker query, there's no cross-agent `agent` filter here).
|
|
ListReminders,
|
|
/// Cancel one of this agent's own pending reminders by id, before it
|
|
/// fires.
|
|
CancelReminder { id: i64 },
|
|
/// This agent's pending-reminder count — used by the pre-`remind` cap
|
|
/// check and the harness's own turn-stats sink.
|
|
CountPendingReminders,
|
|
/// Scheduled/delivered/pending reminder counts over a trailing
|
|
/// `since_secs` window (`0` = all time) — the harness's own `/api/stats`
|
|
/// 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.
|
|
/// `wake_prompt`, when set, is driven as a synthetic follow-up turn once
|
|
/// the compaction actually finishes — the dashboard button's own
|
|
/// requests go through `Bus::request_compact` directly with `None`, not
|
|
/// through this variant, since a human watching the dashboard isn't
|
|
/// waiting on a wake.
|
|
Compact { wake_prompt: Option<String> },
|
|
}
|
|
|
|
/// A response on the in-agent socket. Serialised with a `kind` tag,
|
|
/// mirroring the core↔agent protocol's response shape.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum Response {
|
|
/// Op succeeded, no payload.
|
|
Ok,
|
|
/// Op succeeded and touched `count` rows (clear / mark-done).
|
|
Acked { count: u64 },
|
|
/// `ListTodos` / `ListReminders` result (each wraps its rows as the
|
|
/// matching [`LooseEnd`] variant).
|
|
LooseEnds { loose_ends: Vec<LooseEnd> },
|
|
/// `CountPendingReminders` result.
|
|
PendingRemindersCount { count: u64 },
|
|
/// `ReminderRollup` result.
|
|
ReminderRollup {
|
|
stats: hive_sh4re::inbox::ReminderStats,
|
|
},
|
|
/// Op failed; `message` is operator-facing.
|
|
Err { message: String },
|
|
}
|