Chio/Docs
LOGIN · JOIN

PlatformDurable State

Node

Retention & Archive

A maintenance thread archives a checkpoint-aligned prefix to a sibling database, deletes it from the live store, and refuses every rotation it cannot back.

Where signing lives, and where deleting lives

Receipts & Audit owns the receipt itself: its signed body, canonical JSON, and the Merkle checkpoint that commits a batch. This page owns the only path in Chio that removes a signed receipt from a live database, and everything that has to be true first. The file mechanics underneath both, the single writer and the schema stamp, are Node State on Disk.

Deleting evidence without breaking it

A receipt database is append-only by construction. The receipt tables and the claim_receipt_log_entries projection carry reject_delete triggers that RAISE(ABORT), every persisted checkpoint’s Merkle root is rebuilt from that projection on verification, and the projection is cross-checked against the source tables by exact receipt-id set equality. A plain DELETE therefore does not lose history, it is refused. Drop the guards first and you get the worse outcome: the next append, the next health check, and the next open all fail on set drift, and a restart does not recover. One drift shape has an in-code repair; it is the last section of this page.

Retention is the supported way out. It archives a contiguous prefix of the log into a second SQLite file, deletes exactly that prefix from the live database in one transaction, and records a watermark that tells every later verifier which range is now checked against the archive rather than rebuilt from the live claim log. Everything here is one process reading and writing its own disk. The mechanism is in the tree: the maintenance thread in crates/kernel/chio-kernel/src/receipt_store.rs, the Rotate writer command and the co-archive-then-delete path in crates/platform/chio-store-sqlite/src/receipt_store/evidence_retention.rs. The design record, docs/architecture/reliability/RFC-0007-retention-without-bricking.md, is still headed Status: Draft and listed as minor-gaps in the reliability index. Read the RFC for why the shape is what it is; read the code for what it does.

Pruning here is what makes the cluster receipt streams non-dense, so a peer cannot distinguish a row this page archived from one it never received: Receipt Aggregation states that as a published non-claim.


One thread, one actor, three durable objects

  • The maintenance thread. RetentionMaintenanceHandle::spawn takes a dedicated Arc<dyn ReceiptStore> clone of the store and one OS thread. It sleeps check_interval_secs in 200ms slices so a stop is responsive, calls ReceiptStore::rotate_receipts inside catch_unwind, records the outcome, and sleeps again. It sleeps before its first rotation, so a freshly started kernel never rotates at boot. Dropping the handle sets the stop flag and joins, so shutdown waits out at most one 200ms slice plus any rotation already running. This thread is the only caller of rotate_receipts in the tree. No CLI verb and no HTTP route forces a rotation.
  • The writer actor. Rotation does not run on the maintenance thread. dispatch_rotate increments the writer in-flight counter, try_sends a ReceiptCommitCommand::Rotate to the same single commit actor that owns appends, and blocks on the reply. Serialization with appends comes free from that queue, with no second lock. The send is non-blocking: a full commit channel refuses the rotation for that interval rather than queueing it.
  • The archive file. A separate SQLite database, stamped with the Chio application_id and the receipt-store schema revision. create_archive_schema creates thirteen evidence tables in it, plus the schema-version table: nine receive copied rows, and the four checkpoint-projection tables are empty shells so that opening the archive as a receipt store does not fail on a missing table. Archive tables carry no REFERENCES clauses; it is a write-once evidence copy, not a live database enforcing cascades.
  • The watermark ledger. receipt_retention_watermark records each rotation’s archived_through_entry_seq, cutoff, and archive path. Three triggers make it append-only and strictly increasing at the database level, because a later verifier trusts this value to skip work.
  • The tombstones. receipt_retention_tombstones keeps one small row per archived receipt id. Deleting a live receipt also deletes its UNIQUE(receipt_id) sentinel, so without a tombstone an archived id looks brand new to the append path’s ON CONFLICT DO NOTHING and could be inserted a second time. Two BEFORE INSERT triggers on the receipt tables abort any insert of a tombstoned id, and two more make the tombstones themselves immutable.

What an operator can actually set

RetentionConfig fieldDefaultMeaningIn chio.yaml?
retention_days90Age threshold. The time cutoff is now - retention_days * 86400.Yes, receipts.retention_days
max_size_bytes10_737_418_240Size threshold, compared against the freelist-adjusted live size, not the on-disk size.No
archive_path"receipts-archive.sqlite3"Archive target. Created on first rotation, then pinned for the life of the store.No
tenant_idNoneTenant scope. Any Some value is refused; see the limits table.No
check_interval_secs3600How often the maintenance thread evaluates a rotation. Clamped to at least 1 second.No
explicit_cutoff_unix_secsNoneInternal. Set by archive_receipts_before to bypass both thresholds. No serialized form exists.No

Four knobs are not in the config file schema

ReceiptsConfig in crates/platform/chio-config/src/schema.rs is deny_unknown_fields and holds exactly three keys: store, checkpoint_interval, and retention_days. The archive path, the size ceiling, the tenant scope, and the check interval are all filled from RetentionConfig::default(); the source comment in to_kernel_config names the first three. Three consequences follow. to_kernel_config always emits retention_config: Some(..), so a file-configured kernel always runs retention, hourly, with no way to retime or trigger it. Its archive lands at the relative default path, which resolves against the process working directory on the first rotation. And because retention is always on, a chio.yaml that sets checkpoint_interval: 0 to disable checkpointing (legitimate under ADR-0008, and unvalidated by the file schema) makes the store attach fail outright. Set these fields programmatically on the returned KernelConfig if you need them.

Attaching a store is where a policy that could never be honored is rejected. try_set_receipt_store_handle refuses three configurations outright rather than spawning a worker that would log a failure every interval: a store whose supports_retention is false, a tenant_id against a store whose supports_tenant_scoped_retention is false (which is every store in the tree, including SqliteReceiptStore), and retention alongside checkpoint_batch_size == 0. The last follows from the mechanism: the watermark only advances to a checkpoint boundary, so with checkpointing off it could never leave zero.

The signer that makes a prefix prunable

Retention can only free what a checkpoint has already committed, so its throughput is the checkpoint signer’s throughput. The same attach that spawns the maintenance thread installs a BackgroundCheckpointSigner { keypair, max_batch } as one more command on the writer actor, with max_batch taken from checkpoint_batch_size (DEFAULT_CHECKPOINT_BATCH_SIZE = 100, and 0 disables checkpointing per ADR-0008). A store that advertises kernel-signed checkpoint support but fails to install a signer has its attach refused rather than appending forever without producing any.

The build runs after commit_receipt_batch has already sent every append durability response, so it does not extend append latency. It is off the append response path, not off the writer thread: it takes the same single writer connection, and it runs before the co-drained flush waiters are released, which is what makes flush_receipt_writes a checkpoint barrier and not merely a durability barrier. Each checkpoint is O(batch) and the loop repeats while the head owes max_batch entries. A build failure, including a caught panic, records last_error and leaves the head untouched; it never fails the append that triggered it, because that append is already durable.


One rotation, in order

rendering
A rotation. Everything before the write lock leaves the evidence intact; the only destructive step is the last transaction, and it re-checks under the lock what the pre-flight already checked.
sourcecrates/platform/chio-store-sqlite/src/receipt_store/evidence_retention.rs:855-964at fe56570

The actor arm refuses before it reads anything if the writer’s verified head is poisoned. Then it runs a full verify_checkpoint_chain_integrity and a full claim-log set-equality validation, in both verification modes. That is deliberate and it is the expensive part. An incremental head attests new appends only; it never notices that a checkpoint-covered source row and its projection row were both deleted out of band after the store opened. Rotating over that drift would co-archive the survivors, delete the rest, and stamp a watermark the archive cannot back. The rebuild is off the append hot path, which is the code’s argument that it is affordable, but it still holds the single writer for its duration. See the limits table for what that costs.

The cutoff comes from resolve_rotation_cutoff. An explicit cutoff wins. Otherwise both triggers are evaluated and the rotation takes whichever frees more; if neither fires, the rotation returns zero archived before reading a single checkpoint. The time trigger fires when the oldest claim-log entry predates now - retention_days * 86400. The size trigger fires when live_db_size_bytes, which is (page_count - freelist_count) * page_size, exceeds max_size_bytes, and its cutoff is one second past the median claim-log timestamp. Both details are load-bearing: excluding freelist pages is what makes a size-driven rotation converge instead of re-firing forever on freed-but-unreclaimed pages, and the extra second is what keeps a burst of receipts sharing one timestamp from blocking their own batch. The number an operator reads is not the number the trigger compares: the in-process health report publishes db_size_bytes (page_count * page_size, freelist included), and the read-only sampler behind chio receipt health publishes neither, nulling that field.

What holds the watermark back

Archival is expressed as one monotone entry_seq watermark W: the largest checkpoint batch_end_seq whose entire covered prefix qualifies. Because checkpoints tile the prefix contiguously, the archived range is always [1, W] and no checkpoint ever straddles it.

crates/platform/chio-store-sqlite/src/receipt_store/evidence_retention.rssql
SELECT COALESCE(MAX(kc.batch_end_seq), 0)
FROM kernel_checkpoints kc
WHERE NOT EXISTS (
    SELECT 1 FROM claim_receipt_log_entries e
    WHERE e.entry_seq <= kc.batch_end_seq
      AND e.timestamp >= ?1
)
AND NOT EXISTS (
    SELECT 1 FROM chio_authorization_receipt_consumptions ac
    JOIN claim_receipt_log_entries ae ON ae.receipt_id = ac.authorization_receipt_id
    JOIN claim_receipt_log_entries ce ON ce.receipt_id = ac.consumer_receipt_id
    WHERE ae.entry_seq <= kc.batch_end_seq
      AND ce.entry_seq > kc.batch_end_seq
)
-- ... three further NOT EXISTS clauses follow, plus a sixth on settle_attempts
-- ... when that table is present; see the table below

The first clause is the age rule, and it reads the whole prefix rather than the boundary row, so one non-monotonic timestamp inside the prefix cannot smuggle an unaged receipt into the archive. The other four are all the same shape: never archive a row that live evidence still points at.

GuardHolds W back whenBecause
AgeAny entry at or below the boundary is not older than the cutoff.The unit of archival is a whole checkpoint batch, not a row.
Authorization consumptionAn authorization receipt is inside the prefix but the consumer that spent it is above it.The binding row is a RESTRICT foreign key on the authorization. Archiving the pair separately would either strand the live consumer’s replay binding or fail the delete.
Receipt lineageA lineage parent is inside the prefix but its child is above it.Parent resolution for governed call-chain validation reads the live receipt tables only. A live child would lose its verified parent.
Settlement / metered reconciliationA receipt in the prefix has a reconciliation still in open or retry_scheduled.Both upsert paths require the live receipt row. Archiving it turns an actionable item into a NotFound.
Settlement attemptsAny settle_attempts row joins a receipt in the prefix. Skipped on stores without that table.Every attempt row is pending or retryable work.

Every guard reads the whole covered prefix, not the boundary row, and that costs liveness as well as buying safety. One receipt with an open reconciliation, or one unresolved settle_attempts row against it, disqualifies every checkpoint whose batch_end_seq reaches its entry_seq. That pins W at the last boundary below it for as long as the row stays nonterminal, and at zero when the row sits in the first batch, however old the rest of the log is. A stuck reconciliation is a stuck retention policy; a flat retention_watermark_entry_seq under a growing database is the symptom.

In incremental mode the result is then capped at the newest checkpoint boundary the pre-flight audit actually verified, so a checkpoint row appended by a second handle since the head was seeded cannot widen the prune. A W of zero, or one that does not exceed the recorded high-water mark, returns zero archived without touching anything.

Copy first, delete once

The archive path is vetted before the ATTACH. A target that evaporates on DETACH, or one that aliases the live database by path, by symlink, or by inode, is refused: the identity checks would compare a table to itself and pass trivially while the delete removed the only copy. The path is then canonicalized, so the ledger records an absolute location a later reader resolves the same way from any working directory.

The copy is idempotent. Append-only evidence uses INSERT OR IGNORE. The two reconciliation tables use INSERT OR REPLACE, because their rows are mutated in place by later reconciliation and a stale archived copy would otherwise fail the identity re-check on every subsequent rotation and stall the prefix permanently.

Then verify_co_archival_complete runs, and presence is not what it checks. For each of nine tables it counts the live prefix rows whose archive counterpart matches on that table’s key column and NULL-safely on every remaining column. Any shortfall raises RetentionArchiveIncomplete { table, live, archived } before a single delete. A count-only check would pass while a reused archive held the right ids under divergent bytes, and the delete would leave no faithful copy behind.

crates/platform/chio-store-sqlite/src/receipt_store/evidence_retention.rssql
DROP TRIGGER IF EXISTS chio_tool_receipts_reject_delete;
DROP TRIGGER IF EXISTS chio_child_receipts_reject_delete;
DROP TRIGGER IF EXISTS claim_receipt_log_entries_reject_delete;

INSERT OR IGNORE INTO receipt_retention_tombstones
    (receipt_id, receipt_kind, archived_through_entry_seq, tombstoned_at)
    SELECT receipt_id, receipt_kind, {w}, {now}
    FROM claim_receipt_log_entries WHERE entry_seq <= {w};

DELETE FROM settlement_reconciliations WHERE receipt_id IN (
    SELECT receipt_id FROM claim_receipt_log_entries
    WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
DELETE FROM metered_billing_reconciliations WHERE receipt_id IN (
    SELECT receipt_id FROM claim_receipt_log_entries
    WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
DELETE FROM chio_authorization_receipt_consumptions WHERE authorization_receipt_id IN (
    SELECT receipt_id FROM claim_receipt_log_entries
    WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
DELETE FROM chio_tool_receipts WHERE seq IN (
    SELECT source_seq FROM claim_receipt_log_entries
    WHERE entry_seq <= {w} AND receipt_kind = 'tool_receipt');
DELETE FROM chio_child_receipts WHERE seq IN (
    SELECT source_seq FROM claim_receipt_log_entries
    WHERE entry_seq <= {w} AND receipt_kind = 'child_receipt');
DELETE FROM claim_receipt_log_entries WHERE entry_seq <= {w};

That batch, the watermark insert, and the recreation of every immutability guard all run inside one BEGIN IMMEDIATE transaction. SQLite executes CREATE and DROP TRIGGER transactionally, so a rollback restores the rows and the guards together and a failed delete can never leave the store append-only in name but not in enforcement. The transaction also re-runs the co-archival identity check, the archive-path pin, and the archive-backing check now that it holds the write lock: the pre-flight versions of all three are fast fails, and a second handle can commit a dependent row or repoint the ledger in the window between them.

The source tables and the projection lose exactly the same receipt-id set in that one transaction, which is why the set-equality validator passes afterward with no exemption of its own. Cross-database atomicity is never required: the copy finishes first, the delete touches only main, and a crash between them leaves the live store intact and the rotation re-runnable. Only then does the actor detach and run PRAGMA incremental_vacuum and PRAGMA wal_checkpoint(TRUNCATE) to give the pages back.

The first rotation on a pre-migration store runs a full VACUUM

SQLite applies a changed auto_vacuum mode only to a brand-new file, so a store predating the pragma reclaims nothing. rotate_on_writer_connection therefore opens with migrate_auto_vacuum_incremental_if_needed, which on a database still reading auto_vacuum = NONE sets the pragma and runs one full VACUUM: a full-file rewrite on the writer connection, needing free disk roughly equal to the database and blocking every append until it finishes. Once per legacy store, and before any cutoff is resolved, so it also runs on a rotation that then archives nothing.

A watermark is trusted, not believed

Chain verification exempts every checkpoint with batch_end_seq <= W from the live Merkle rebuild, since its claim-log rows are gone on purpose. Signature parsing, column agreement, transparency-projection rows, and predecessor linkage all still run; only the rebuild is skipped. That exemption is the most dangerous thing on this page: a forged W would silence verification for the range it names. The ledger triggers enforce monotonicity, and monotonicity is not archival, so trusted_retention_watermark requires three independent facts and returns 0 if any of them fails.

FactCheckAttack it closes
W is a real boundarySome kernel_checkpoints row has batch_end_seq = W.A watermark inflated past the last real checkpoint, exempting never-archived live ranges.
The prefix is actually goneNo live claim-log row survives at or below W.A raw insert of a valid boundary while the covered rows stay live and possibly corrupt.
The archive backs itThe ledger’s archive is opened read-only and, for every covered checkpoint, its archived rows re-derive the signed Merkle root.An out-of-band delete of the live prefix followed by a planted watermark, and a tampered archive that keeps the right entry_seq values under replaced bytes.

The third check ties the exemption to evidence rather than to the absence of evidence, and it is not cheap. archive_path_backs_prefix opens the archive read-only and re-derives the Merkle root of every checkpoint at or below W from the archived rows, so it is O(archived history) and grows with every rotation, forever. It sits inside verify_checkpoint_chain_integrity, which runs when the writer seeds its head at open, in receipt_checkpoint_status behind every health report, and in the rotation pre-flight. Budget for a startup and a health check whose cost tracks total archived receipts, not live ones. The one place it could have reached the append hot path, the adopted claim-log delta check, is gated behind a cheap comparison against the raw ledger value first, so a steady-state append on a rotated store never opens the archive.

One archive per store, forever

The first rotation pins the archive path in the ledger, and a later rotation pointed somewhere else is refused. A deleted prefix can never be re-copied, so a second target would hold only the newer suffix while the older prefix stayed stranded in the first file, and the watermark reader would find the named archive short and withdraw the exemption from a store whose evidence is real but split. For the same reason a rotation refuses to proceed when the archive backing the committed watermark has gone missing, been emptied, or stopped re-deriving its roots. Restore it before rotating again.

What a rotation refuses

ConditionResult
The writer’s verified head is poisoned.Conflict, pointing at chio receipt audit --repair. Nothing is read or deleted.
The checkpoint chain or the claim-log projection does not validate.That validation’s own error, before any cutoff is computed.
tenant_id is set.RetentionTenantScopeUnsupported, checked twice: in dispatch_rotate before the command is queued, and again on the writer connection.
A copied row does not match the live row column for column.RetentionArchiveIncomplete. Re-checked under the write lock, so a row that escaped the copy window also aborts.
The archive target is :memory:, a mode=memory URI, empty, or an alias of the live file.Conflict, before the ATTACH.
The archive path differs from the one the ledger pinned.Conflict naming both paths.
The archive no longer backs the committed watermark.Conflict instructing the operator to restore the archive first.
The archive carries a foreign application_id or a newer schema revision.Conflict, in create_archive_schema.
A new mark would not strictly exceed the current one.Returns 0 archived. A bypassing raw insert is aborted by the ledger trigger; RetentionWatermarkRegression covers the helper path.
The writer commit channel is full, or the actor is gone.Pool("sqlite receipt commit queue saturated") or the actor-unavailable error, from the try_send. The interval is skipped; the next one retries.
The rotation panics.Caught at two levels. The actor’s catch_unwind returns a typed writer-panic error and takes a fresh connection for the next command; the maintenance thread catches anything that escapes and retries next interval.

A failure does not stay in the logs. The worker calls record_retention_rotation_outcome with the message, which lands in ReceiptStoreHealthReport.retention_error and forces healthy to false for as long as it is set, so a store serving under a retention policy that is not being honored cannot read green. A later success clears it. This is in-process state, and it is the reason chio receipt health is not enough on its own: receipt_store_health_read_only reads retention_watermark_entry_seq off the file, but hardcodes retention_error: None, defaults the whole writer block, and nulls db_size_bytes, because a read-only observer cannot see the owning writer’s memory. A store whose retention has been failing every hour for a week still prints healthy there. Read the failure from the process that owns the database. For how a degraded writer is judged, see Health & Readiness.


Guarantees and limits

StatusClaimEvidence
ShippedRetention runs on the single writer actor. No archival statement executes on a reader-pool connection.dispatch_rotate and the Rotate arm; test reader_pool_never_rotates
Proved by testOver random interleavings of tool appends, child appends, and rotations, the store stays appendable, reopenable, and healthy, and the archived and live receipt-id sets partition the full appended history with no overlap and no loss.prop_retention_preserves_append_invariant, a state-machine proptest
Proved by testAfter a rotation, the next append, a fresh open(), checkpoint status, and further checkpoint creation all succeed. This is the brick RFC-0007 was written against.retention_then_append_and_reopen_succeeds, checkpoint_chain_watermark_exemption
Proved by testA watermark that lands on a real boundary but is not backed by an archive, or is backed by a tampered one, does not skip verification.bogus_watermark_does_not_skip_verification, watermark_trust_requires_backing_archive, watermark_trust_rejects_tampered_archive_contents, boundary_matching_watermark_over_live_prefix_does_not_skip_verification
Proved by testA dependent row committed into the archived prefix after the copy but before the delete takes its lock survives; the delete fails closed instead of removing it un-archived.delete_fails_closed_when_a_dependent_row_escapes_the_copy, delete_rechecks_archive_path_under_the_write_lock
Proved by testA size-triggered rotation shrinks the measured live size, and repeating it under the same config does not run away: the retry either archives nothing or does not grow the store. The test asserts convergence, not that one pass lands under the threshold.size_rotation_converges_below_threshold, size_rotation_archives_when_median_timestamp_is_shared
Proved by testAn archived receipt id cannot be appended again, and the tombstone recording it cannot be updated or deleted.archived_receipt_id_cannot_be_reappended, retention_tombstones_are_immutable
LimitThe live store’s evidence export does not reach into the archive. build_evidence_export_bundle reads the live tables only, so an archived receipt yields no record and no inclusion proof from the rotated store. Proofs for an archived range mean opening the archive file itself as a receipt store, which the retention tests exercise and no CLI verb does.collect_inclusion_proofs_for_export over receipts_canonical_bytes_range (live connection)
LimitThe archive is load-bearing after the first rotation. Losing it does not corrupt the live store, but it withdraws the verification exemption for the range it held, and full verification then fails because the live prefix is intentionally gone. Back it up with the database.archive_path_backs_prefix; RFC-0007, risks and alternatives
LimitA rotation pauses appends for longer than the delete. The pre-flight is a full claim-log validation (loads and sorts every source and projection row) plus a full chain verification (a Merkle rebuild per checkpoint above W, plus the archive re-hash below), so the writer is held for O(live history + archived history) every interval, not O(batch). Only the delete and incremental_vacuum scale with the batch.The Rotate arm; validate_claim_receipt_log_entries, verify_checkpoint_chain_integrity
LimitArchive verification cost is unbounded in time. Every full chain verification re-derives the signed root of every archived checkpoint from the archive, so open, health, and rotation all get slower as the archive grows and never get faster.archive_path_backs_prefix loops all covered checkpoints
LimitAny one blocked row freezes everything above it. The age, lineage, consumption, reconciliation, and settle-attempt guards all read the full covered prefix, so a single nonterminal reconciliation low in the log pins W at the boundary below it indefinitely.compute_archival_watermark, the NOT EXISTS clauses
LimitNo operator command triggers a rotation. The kernel maintenance thread is the only caller, on an interval that is not expressible in chio.yaml, so a file-configured deployment gets an hourly rotation it can neither force nor retime.Sole call site in RetentionMaintenanceHandle::spawn; no retention verb in chio-cli beyond repair
LimitRetention cannot free anything a checkpoint has not covered. With checkpoint_batch_size large relative to traffic, the uncheckpointed tail grows unbounded regardless of retention_days.compute_archival_watermark reads kernel_checkpoints only
UnsupportedTenant-scoped retention. A tenant is not a prefix of the claim log, so it cannot be expressed as an entry_seq watermark without punching holes in checkpointed ranges. Every store in the tree reports supports_tenant_scoped_retention() == false; the attach is refused and the rotation fails closed unmodified.RetentionTenantScopeUnsupported; tests tenant_scoped_rotation_rejected, tenant_scoped_rotation_is_rejected_and_leaves_counts_unchanged
UnsupportedRemote receipt operator operations, reads included. Every receipt verb, from health through retention repair, resolves a local --receipt-db and rejects --control-url outright. Run them on the node that owns the file.local_receipt_db_path in chio-cli; docs/release/OPERATIONS_RUNBOOK.md (whose quoted error string has drifted from the code)
Not in the file schemaArchive path, size ceiling, tenant scope, and check interval, per the callout above. A chio.yaml deployment gets the defaults for all four.crates/platform/chio-config/src/schema.rs
Design onlyThe RFC records an archive_sha256 column on the watermark ledger. The column exists; the rotation and repair paths both write None into it, so archive identity is established by re-deriving signed roots, not by a stored digest.insert_receipt_retention_watermark call sites

One recovery path exists, for a store damaged by a delete that left orphaned projection rows behind. chio receipt retention repair --archive <path> opens through open_existing, so the tool does not brick trying to diagnose a bricked store. It removes only claim-log rows absent from both source tables, only when the named archive holds a byte-identical copy of each, only up to the smallest checkpoint boundary covering them, and only when every row in that boundary’s prefix is itself an orphan whose archived rows re-derive the signed roots. If a watermark already covers that boundary, the --archive you pass must be the one the ledger pinned, because the tombstones are stamped from it. Anything else aborts untouched, and both output modes exit non-zero when the drift survived. Nothing here recovers the other shape, a delete that took the source rows and their projection rows together; that store fails the rotation pre-flight and needs a restore.


Next Steps

  • Receipts & Audit · the signed body, canonical JSON, and the checkpoint whose boundary this page prunes to
  • Node State on Disk · the single writer, the reader pool, and the schema stamp every rotation runs through
  • Health & Readiness · the supervised commit writer whose liveness a failing rotation shares, and what readiness denies on
  • Backup & Restore · why the archive is now part of the backup set, not an optional extra
  • Node Overview · what else one process owns on its own disk
Retention & Archive · Chio Docs