§12c. Journal headroom and ENOSPC
planarian @ a924e09 · Spec v4.6 · snapshot main · all versions
- Format. Records are length-prefixed and CRC-32C-checked; a torn or mismatched tail record is ignored and truncated on open, never interpreted as a transition. Anything worse — a checkpoint that fails its CRC, does not parse, or violates an invariant on load, or a retained record that does — is an
Integrityfailure (§12d), reported as such by both processes; it is never reported as the journal being merely absent. Every record also carries the hash of its predecessor (I18), and the checkpoint carries the chain head, so an edit to any retained record is detected on the next read and raised as anIntegrityfailure (§12d). The chain is tamper-evident, not tamper-proof, until E signs the chain head with a key outside the running system. The journal is a preallocated file (default 64 MB) in/var/lib/planarian/journal/. - Journal versions (owner decision, Nov 3). Two classes of record kind. Every journal record kind is one of: informational: no decision reads it; decisive: the fold uses it to decide what runs, what is held, or what is approved. Hold events and withdrawal links are decisive. The class is in the frame. Each record’s frame carries its kind’s class, so a build can classify a kind it doesn’t know. Opening a journal with an unknown kind. An unknown informational record is skipped, and the skip is journaled (
Skipped { kind }). An unknown decisive record is anIntegrityfailure. Rollbacks across a format change. A generation transition whose targetplanariadcannot read a decisive record kind present in the journal, or a decisive field of one (a field added to an existing kind that the fold decides from), asks first (AskFirst,ApproverClass::Administrator). The approval names what the target would not understand, so a rollback never silently lands a machine inIntegrityor drops a decision. Each build exports the kinds and decisive fields it reads (journalKinds). A generation from before the export is taken to read only the journal format’s original set. - Chain head. The chain is unkeyed in A: it detects damage and partial edits, not a consistent rewrite by root (I18). The current head is included in every signed health report (§10), so an off-machine receiver that keeps the heads it has seen can detect a rewrite of anything it has already witnessed (C, with the mesh); measured boot anchors it on the machine in E.
- Checkpoint. Compaction is not deletion of history; it is checkpoint-then-truncate. A checkpoint captures all live state the journal establishes: the external desired-state source’s last accepted sequence number (§9), active overrides (including
Until::Explicitones from long-committedStopAndSuppress), the security floor, open and cooling incidents with their budgets and regression counts, the nonces of consumed approval tokens within their validity window (outstanding tokens are not carried: a restart invalidates them, §12 step 4), theAgentStartedrecords of live agent instances (I24),known_good/accepted_baseline/live_tested, every non-terminal transaction with its lease, the lease epochs of recently terminal ones, and the recovery interlock: the set of unresolvedRecoveryFailedtransactions with the facts needed to resolve them (what was being changed, which step failed, the observed state, the base and candidate override hashes, the revert target). The interlock is live control state, not history: while it is non-empty, autonomous mutation is blocked (onlyObserve-tier reads, user-authorizedSetOverride/LiftOverride, and administrator operations run), and it survives compaction and restart of both processes. It is cleared only byplanarian resolve <txn> --as-committed | --as-reverted | --manual-repaired, an administrator-authorized operation journaled as a transition of its own; no other request clears it. Gym case: inject a failed revert, force compaction, restart daemon and watchdog; autonomous mutation stays blocked until the resolution is durably recorded. It is written to a temp file, fsynced, renamed overcheckpoint, directory fsynced; only then is the journal truncated to records after the checkpoint’s sequence number. Recovery from (checkpoint + retained journal) MUST reconstruct the same state as recovery from full history; the gym asserts this by comparing both reconstructions after forced compaction. Compaction triggers at 75% full. - Filesystem contract. The preallocation guarantee holds only where in-place rewrite does not allocate: on ext4 and XFS a
fallocated file; on btrfs only with the journal directory markednodatacow(chattr +Cbefore creation) and the file preallocated; on ZFS it does not hold, and the daemon instead reserves space with a dedicated datasetreservation. The installer applies the layout-specific setup and the gym validates it per supported layout (ext4, btrfs, ZFS). If a recovery-path write to the preallocation fails with ENOSPC despite the contract, the daemon writes a fixed-sizeRecoveryFailedsentinel into a separate 4 KB file reserved at install and blocks all mutation. The reservation is counted indevice:fs-journal’s Readiness probe. CollectGarbage { emergency: true }is the only transition allowed to start whiledevice:fs-journalordevice:fs-nixis below threshold; it is Runtime and its record fits the preallocation.- ENOSPC behavior by state: in
Prepared/Staged/Built(write to stage or store fails) → reject withNoSpace, open the storage incident, do not retry the original until it closes. InActivating/Checking→ treat as probe failure →Reverting; the revert record is in the preallocation. InReverting→ if a revert step fails for space, markRecoveryFailedvia a fixed-size sentinel record and alert. Promotion (temp-write of the override file) failing for space → treat as Checking failure → revert; the live override file is untouched because the rename never happened. /bootis tracked separately:publish_bootfailing for space →Checkingfailure → revert; the candidate is never left as the only boot entry.
Gym cases: fill the journal filesystem to the reserved-block boundary as the daemon’s uid; verify that emergency collection is recorded and executed, or that mutation stops with an explicit RecoveryFailed/NoSpace status; repeat after a snapshot and after repeated journal reuse on btrfs; force compaction with a live Explicit suppression and an exhausted incident budget, then crash and restart (both survive); interrupt a record write and a checkpoint write (neither is misread).