2026-08-29 · Updated 2026-08-29 · 10 min read
Immutable archives for large agent-task histories
Move finished agent tasks into sealed, verifiable cold packs without losing a byte of evidence: revision-bound plans, content-addressed packs, read-only resolution, tamper evidence, and a maintenance rhythm that keeps hot state fast.
By Juno AI INC · yylo-ledger · archives · task-history
Every task an agent finishes leaves two things behind: a Markdown file and an append-only history segment. Both are cheap on day one and neither ever shrinks. A year of parallel agents later, every discovery query — every listing, every readiness scan — is paying to walk past work nobody will touch again, while the evidence those files hold becomes more valuable, not less, precisely because nobody touches it. Deleting history to buy speed destroys the audit trail; keeping everything hot taxes every working day to preserve it. The resolution is a tier boundary with real teeth: hot state stays small and mutable, terminal history moves once into sealed packs that verify themselves on every read. This page documents that machinery end to end — the plan that authorizes a move, the transaction that executes it, the read paths that replace it, and the failure behavior at each seam. Every behavior that follows was executed in one live session (2026-08-29) run against the ledger release bound by the committed package-facts file this site generates; the first command block is reproduced in its shipped documentation form, and the refusal transcript later is verbatim. The board's finished work genuinely predates its archive plan by months, and a fresh session mints fresh IDs, commits, and hashes.
Hot state and cold history are different jobs
The shipped boundary sentence is worth reading slowly: "Boards can explicitly move up to 1,000 old terminal tasks at a time from hot Markdown/ledger files into bounded, immutable NDJSON packs." And the read-side contract that keeps the split honest: "Default list, search, ready, and order remain hot-only; exact get ID and history ID transparently verify and read either tier." Discovery answers never decode pack bytes — the cold half of their derived index is rebuilt, verifying every pack, only when the archive tree itself changes; exact reads never pretend the cold tier is missing. What actually moves is not a summary — each pack record is an envelope holding the task's final state, its complete event history, the moment it was archived, the moment it became terminal, the source revision it left, and three digests: the task's semantic hash, the history's hash, and a record hash computed over every other envelope field, which is what makes a record self-verifying rather than merely labeled.
Two rules govern what is even eligible. A task must be terminal — finished or already soft-archived — and its age is measured the strict way: the plan re-derives when the task *became* terminal from its recorded history, the module's own contract being to return "the latest proved transition into the current terminal status", not to trust a wall clock or a stale date field. A task that finished this morning fails that gate even though its status looks identical to its neighbors'. And a terminal task someone still depends on does not block the move — archiving a blocker is itself a resolution, which the recorded session demonstrates below. The memory boundary — what this task state is and is not — is durable agent memory's definition to give; this page only moves its terminal tier.
Archival is an authorized transaction, never background work
"Archival is never automatic." Nothing ages out on a timer, no daemon sweeps the board, and the word *explicitly* in the boundary sentence above is doing real work: an archive run is an owner-authorized mutation of the repository, gated before it starts. The gates, as shipped — a clean worktree and index, a Git HEAD to bind to, report paths that must live outside the repository, and any explicit reservation receipts, which hold tasks that active work still cites:
The plan is the review surface, and it is sealed: it hashes its own policy, binds the source HEAD, the board configuration, the reservation receipts, and the existing cold inventory, and it lists every selected task with the revision it selected. It also lists what it refused, with reasons — and the recorded plan refused three tasks for three different causes. The still-open reporting task came back status; the morning's fresh completion came back too_young, the age gate doing exactly what its ledger-derived definition promises; and the release-audit task came back reserved, because an active receipt named it. Nothing was silently dropped — a maintainer can read the rejection list and know the hot board's shape after the run.
Then the plan goes stale on purpose. "Plans are revision-bound and fail closed when Git/config/reservations/archive inventory or selected task history changes." Between planning and activation, the recorded session committed one unrelated follow-up task — and the sealed plan died at the door, transcript verbatim with the launcher spelled as run:
This is the correct failure, and it is not a retry away from success: the plan selected specific task revisions under a specific HEAD, and activating it after the world moved would archive a board that no longer exists. A plan is also single-use in the happiest direction — the activation commit moves HEAD itself, so replaying the same plan file fails with the identical stale refusal, as the recorded second attempt shows. When anything moves, you re-plan and review again; the tooling refuses to guess on your behalf.
Packs are sealed, content-addressed, and self-verifying
Activation is one path-owned Git transaction — the module's own description is "Execute one path-owned hot-to-cold Git transaction" — and what it produces is three files per pack: the NDJSON pack, a manifest, and a checksum sidecar. "The NDJSON pack is canonical." — the sidecars are derived from it, and a lost manifest or checksum can be rebuilt without ever reading hot task storage. The pack holds sorted records; the manifest is the canonical inventory — each record's offset, length, record hash, and terminal transition — plus provenance: the source HEAD, the config hash, the creating version, the byte bounds; the sidecar binds the manifest bytes and the pack name to the pack's content. And the name is the content: every pack is named for its own digest, so two packs with identical bytes are identical files and a renamed or truncated pack fails its own filename.
The recorded activation made the layout concrete. Planning again with deliberately tight byte bounds — records around three and a half kilobytes against a target of two thousand — the plan split its three selected tasks into three batches, and activation wrote three one-record packs, all stamped with the same second, each carrying its own digest in its name, plus one commit titled for the count and the plan: Archive 3 terminal Kanban tasks [10273b8ebd69], adding nine pack files under a dated archive directory and deleting three task files and three history segments. The receipt, written outside the repository, names the plan hash, the parent commit, the archive commit, one digest per pack, and the revert command — git revert plus the commit — which is the entire undo story, because the packs and the deletions landed in that one commit. Bounds are honest at the edges too: an oversized record is never silently truncated or split, since the codec's rule is "Deterministically split records; an oversized record is always isolated" — an oversized record gets its own pack, flagged in the manifest. And creation is exclusive by construction — "Existing output is refused rather than replaced or appended." — packs are written once with exclusive-create and read-only permissions, so there is no code path that rewrites a sealed artifact.
Cold reads verify; cold writes do not exist
After activation, the recorded board split exactly as the contract promises. The listing, a tag search, and a readiness scan all answered from hot state alone: the reserved release-audit task still there, the three archived tasks gone from every answer — and the dependent task, whose blocker had just been archived, appeared in the ready set the same instant, its admission unblocked by the terminal transition. An exact get of an archived ID returned the full record — body, response, commit pointer — and history returned the complete event chain, both verified through the sealed manifest and pack digest on the way in. For scans that explicitly want cold bytes, the CLI ships "Explicit bounded search of immutable cold tasks", with the same projection, limit, and cursor discipline as hot search. The public exact-lookup contract is one line in the source: "Transparent public exact lookup across the one canonical tier."
Writes are the mirror image. The recorded attempts to reopen and to re-archive an archived ID both refused with the same sentence: "Task 9jZ3t3 is in an immutable cold archive and cannot be changed or reopened; create a new task with related_tasks referencing the archived ID". That refusal is the follow-up policy enforced by the only component that can enforce it — not a convention in a contributing guide. "Archived IDs stay terminal and globally reserved; create a new hot task related to the archived ID for follow-up work.", and the planner honors the reservation on both sides: a cold ID blocks case-insensitive reuse, and a plan that would create a hot/cold duplicate is refused at selection. Tampering is detected the same way. The recorded drill flipped one bit near the end of a sealed pack: the archive doctor answered ok: false naming the file and the broken invariant, an exact read of the affected ID failed closed, and restoring the bytes from Git returned the archive doctor to ok with a clean tree — which is the whole recovery model for a damaged pack, because "Sealed packs/manifests must never be edited or appended."; they are Git objects like any other, restored by Git, never repaired in place.
Failures never leave the boundary half-crossed
An archive activation is a distributed move — copy cold in, delete hot, commit both — and its fault contract is stated where it is enforced: "All faults before the commit restore exact hot bytes and index state." Before the commit, staged packs live outside the working tree, hot files are backed up byte-for-byte, and any failure resets the index, restores every hot file with its mode, removes every staged artifact, and leaves the board exactly as it was — the recorded staleness refusal above is the cheapest member of that family. After the commit, the strategy inverts deliberately: the operation leaves a machine-written freeze record in place and stops, so a later recovery can tell a committed transition from an uncommitted one instead of guessing, and the recovery module's contract is that "Recovery never guesses across the commit boundary" — the recorded commit must be the exact current HEAD and its parent must be the sealed plan's source. Inside one activation the tooling also verifies itself: every staged pack is re-verified from independently read bytes before anything is activated, the plan is re-verified against live state again after staging, linked worktrees are checked for edits to the selected paths, and after the commit both doctors — the archive doctor and the hot-board doctor — must pass before the operation reports success. The recorded session completed without tripping any of these paths, and that is stated as a fact about the recording, not as a claim that faults cannot happen; the fault behavior above is the shipped, source-stated contract.
A rhythm that keeps the hot board small
The defaults describe the intended cadence: terminal statuses only, a ninety-day age floor, a thousand tasks per run, a twenty-five-megabyte target per pack with a hard ceiling above it. In practice a team runs this on a rhythm — quarterly works — reviews the plan's selection and rejection lists, and lands one archive commit per run; the repository accumulates content-addressed packs under dated directories while the hot board stays the size of active work. The recorded session closed the loop on its own edge case: the reservation that had held the release-audit task hot was lifted, the next plan selected exactly that task, and a second archive commit moved it into its own single-record pack — after which both doctors read ok, an exact get still answered from the new pack, and the working tree stood clean. Daily board usage — the queries and mutations that never touch the cold tier — is what the task-truth guide walks through, and the complete command reference, installation through archive-search included, is the Ledger documentation.