diff --git a/docs/persistence.md b/docs/persistence.md index 7024b115..dda2a6be 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -79,16 +79,13 @@ header/targets split, and the per-agent power-intent registry: `scheduled_prompts(id)` — requires `PRAGMA foreign_keys = ON` per connection (set at open). - `agent_power` — one tiny row per agent: `agent PK / wanted (up | - offline) / updated_at` — the durable power *intent* behind the job - queue's desired-state reconciliation - (`docs/coordinator.md::Job queue`; owner: `hive-c0re/src/stores/power.rs`). - Written synchronously by every operator/agent power action - (dashboard start/stop, `hivectl stop`, the MCP kill/start tools, - spawn approval); read by `Reconcile` nodes and the boot reconcile. - Intent survives hive-c0re restarts — in-flight queue work - deliberately does not. Agents without a row are seeded from - observed state on first touch (running ⇒ `up`); destroy deletes - the row. + offline) / updated_at`, owned by `hive-c0re/src/stores/power.rs`. + This is the durable power *intent* the job queue reconciles the + observed container state against; intent survives hive-c0re + restarts even though in-flight queue work doesn't. See + [`docs/coordinator.md`'s Desired-state + section](coordinator.md#desired-state-spec-vs-status) for who + writes and reads it and how reconciliation works. Retention: @@ -190,12 +187,10 @@ Shape: The turn loop is the only writer today, but it still goes read-modify-write under a shared in-process lock and merges into the existing object rather than reconstructing it — so a second writer -preserves fields it doesn't own, and the lock closes the lost-update -window between a writer's read and its rename. (The forge notification -poller used to be that second writer, for a delivery-dedupe cursor. It -persists nothing now — forge's own read-state is the durable record of -what has been delivered — and it is a separate process, which an -in-process lock could not have serialised anyway.) +would preserve fields it doesn't own, and the lock closes the +lost-update window between a writer's read and its rename. The lock +is in-process only, so it wouldn't serialise a writer running as a +separate process; none of today's writers are. hive-c0re reads this file on each `build_all` sweep (~10s) via `container_view::read_harness_flags`. Falls back to the legacy individual @@ -209,11 +204,12 @@ Full stdout + stderr capture for every `nixos-container` / `nix build` invocation the lifecycle layer fires. One row per invocation; the row accumulates lines as the child runs. -Replaces the legacy 32-line stderr ring buffer that `lifecycle::run` -kept. The ring tail routinely truncated real eval errors ("tried -alternatives" blocks alone are often 30+ lines), so failures bailed -with an arbitrary tail whose full stream only lived in the host -journal. With this table the dashboard can surface the entire log. +Capturing the full stream (rather than a short tail buffer) matters +because real eval errors routinely run long — "tried alternatives" +blocks alone are often 30+ lines — so a truncated tail would cut off +the actual failure and leave only the host journal holding the +complete output. With this table the dashboard can surface the entire +log. Two indices: - `(agent, started_at)` — backs the per-agent latest-N lookup used @@ -323,11 +319,9 @@ notes, clearing a stuck sentinel) as well as reading it. **`harness` is not mounted at all.** It holds the child's own runtime material — `bash-tasks/`, the turn-stats and event sqlite dbs — and -nothing argues for a parent reading it, let alone writing it. It used to -be mounted RW for "the same management reasons" as `state`, which was -never an argument so much as the side-effect of one loop treating all -three dirs alike. hive-c0re reads a child's harness dir **directly on the -host** when it wants those stats, which needs no mount into the parent. +nothing argues for a parent reading it, let alone writing it. hive-c0re +reads a child's harness dir **directly on the host** when it wants +those stats, which needs no mount into the parent. **`config` is read-only, including for the parent.** A config change is a PR on the child's config repo, made from a clone and merged after @@ -339,8 +333,9 @@ boundary a convention rather than a permission. ⚠️ Not to be confused with the seeding done when an `InitConfig` approval resolves: that writes the child's initial config repo as **hive-c0re, against the host path**, and `read_only` on a bind -constrains writers *inside* a container only. The two are unrelated, and -reading them as the same thing is what kept this mount writable. +constrains writers *inside* a container only. The two are unrelated — +conflating them is an easy way to reason your way into thinking this +mount should be writable when it shouldn't. Per-child isolation still holds: a container only ever has its *own* dirs plus its direct children's bind-mounted, never a sibling's. @@ -396,25 +391,21 @@ deleted. ## Destroy vs purge -- `DESTR0Y` (default) — stops + removes the nspawn container, - drops the systemd drop-in, fails any pending approvals. State - dirs stay put; the agent appears in the dashboard's K3PT ST4T3 - section as a tombstone with `⊕ R3V1V3` and `PURG3` actions. - `R3V1V3` queues a Spawn approval that reuses the kept state on - approve (no re-login). -- `PURG3` (opt-in via the dashboard button or - `hivectl agent destroy --purge`) — DESTR0Y plus wipes - `/var/lib/hyperhive/{agents,applied}//`. Config history, - claude creds, /state/ notes, and the harness dir are all gone. - No undo. +See [For operators](#for-operators) above for what each action does to +an agent's state. The mechanics, for completeness: -The root/bootstrap container is **imperative** infrastructure — managed -end-to-end by hive-c0re, not declared in the host's NixOS config. -`auto_update::ensure_root_agent` recreates it on the next hive-c0re -startup if it's absent (bypassing the approval queue, as required -infrastructure). A soft policy guard in `actions::destroy` currently -refuses to destroy it; even without that guard, destroying it would only -be transient — hive-c0re brings it back on the next startup. +- `DESTR0Y` also drops the systemd drop-in and fails any pending + approvals; the tombstone's `⊕ R3V1V3` button queues a Spawn approval + that reuses the kept state on approve. +- `PURG3` wipes `/var/lib/hyperhive/{agents,applied}//` — the + union of everything `DESTR0Y` left behind. + +The root/bootstrap agent's specialness is implemented as a soft policy +guard in `actions::destroy` that refuses to destroy it, backstopped by +`auto_update::ensure_root_agent`, which recreates it on the next +hive-c0re startup if it's ever absent (bypassing the approval queue, +as required infrastructure) — so even without the guard, destroying it +would only be transient. ### btrfs subvolumes for `/var/lib/hyperhive/agents/` @@ -443,6 +434,11 @@ until an explicit opt-in upgrade. Per-subvolume disk-usage accounting and optional quotas are a follow-up (the qgroup work), not part of the base migration. +This is the same subvolume `hivectl agent subvol snapshot push` +sends to the swarm's snapshot store — see +[`docs/snapshot-store.md`](snapshot-store.md) for what a pushed +snapshot contains and how the store authenticates a sender. + ## Run-time dirs `/run/hyperhive/` is tmpfs-backed (systemd `RuntimeDirectory=`) but