Storage model

What slivingdoc keeps in the bucket — the current manifest, immutable packs, conditional replacement, pack integrity, checkpoints, retention, and cleanup.

The bucket is the durability boundary. Everything on your disk — the visible notes directory, the private Git repository, the pack cache — can be thrown away and rebuilt from the bucket. Nothing on disk is authoritative, and the local caches never decide accepted state.

What one notebook looks like in the bucket

One configured prefix holds exactly one notebook:

<prefix>/current
<prefix>/packs/checkpoints/<through-generation>-<checkpoint-id>.pack
<prefix>/packs/increments/<generation>-<publication-id>.pack

There is no bare repository and no .git directory in the bucket, and the server never uses a bucket listing to decide which state is current. The prefix itself is empty or a slash-separated relative key prefix with no leading or trailing slash, no empty segment, no backslash, and no . or .. segment; an invalid prefix is refused by configuration.

Pack keys carry their target or through-generation and a unique publication or checkpoint ID. Both IDs are UUIDv7 values in the canonical lowercase 8-4-4-4-12 text form, validated for version 7 and the RFC 4122 variant. Ordering is defined by the generation fields, never by UUID ordering. One writer owns each key, and accepted pack bytes are never changed.

Every pack upload also writes slivingdoc-sha256, slivingdoc-size, slivingdoc-kind (increment or checkpoint), and slivingdoc-generation as S3 user metadata. That metadata helps diagnose and resume uploads, but the manifest descriptor is what is authoritative. A small pack can go up in one request; a large checkpoint pack can use multipart upload. A retry first inspects the object already at that unique key and accepts the existing bytes only when the recorded SHA-256 and size both match.

The current manifest

current is the only authoritative state index. It names one active checkpoint and every accepted increment after it:

{
  "version": 1,
  "generation": 8121,
  "head": "<git-object-id>",
  "checkpoint": {
    "id": "<checkpoint-id>",
    "publication": "<publication-id-of-head>",
    "throughGeneration": 8119,
    "head": "<git-object-id>",
    "key": "packs/checkpoints/8119-<checkpoint-id>.pack",
    "sha256": "<pack-sha256>",
    "size": 123456
  },
  "increments": [
    {
      "generation": 8120,
      "publication": "<unique-publication-id>",
      "parent": "<git-object-id>",
      "head": "<git-object-id>",
      "key": "packs/increments/8120-<publication-id>.pack",
      "sha256": "<pack-sha256>",
      "size": 4096
    }
  ],
  "retained": []
}

A version 1 manifest carries a required integer version of 1, one monotonic unsigned 64-bit generation, the accepted head object ID, one active checkpoint descriptor, an ordered incremental tail, a checksum and size for every pack, and the retained generations described below.

Generations are strict. An absent current is the implicit remote-empty state at generation 0; the first successful conditional creation writes generation 1; every successful replacement, including a checkpoint replacement, increments the observed generation by exactly one. Generation values never reset, and an overflow rejects the operation.

The manifest is read strictly as well. An unknown field, a duplicate field name, a missing required field, and an explicit JSON null are all rejected at every object level, with duplicate names detected before any value is decoded. A future incompatible schema uses a different version, and a version 1 reader rejects it before it touches a single pack.

Validation runs before any pack is accessed, and it checks the whole chain, not just the shape: the checkpoint cutoff is not beyond generation, active increment generations increase and are above the cutoff, the first increment’s parent equals the checkpoint head, each later increment’s parent equals the preceding head, the top-level head equals the last increment head (or the checkpoint head when the tail is empty), each key matches its namespace pattern and contains no backslash, empty segment, . or .. segment, and every object key and checkpoint ID is unique across the manifest. Each retained generation validates as an independent chain under the same rules.

Conditional replacement

The first manifest write uses If-None-Match: *. Every later write uses the exact ETag returned by the preceding read, in If-Match. HTTP 412 maps to the semantic precondition error — expected contention, to be merged and retried. A timeout or connection error after the request bytes were sent is an ambiguous result, not a precondition failure, and is resolved by reading current and looking for the proposal’s publication ID.

Note: An S3 ETag is only a concurrency token for current. It is not a content checksum, and multipart ETags are not reliable digests. Content is protected by the SHA-256 in the manifest descriptor.

This is the whole of the concurrency control. There is no LOCK.lock object, no lease, no renewal loop, no stale-lock recovery, and no clock-based expiry. See A lost race for the writer’s view of it.

Immutable packs and pack integrity

A pack is an immutable Git pack file. An increment holds the objects of one accepted publication and may depend only on the indexed packs before it. A checkpoint is closed: it reconstructs a complete notebook state without older packs or external base objects, and uses no pack delta whose base object exists only in an older pack. Importing a checkpoint pack alone into an empty repository must make its commit and complete file tree readable.

V1 repositories use Git’s SHA-1 object format, so every object ID is exactly 40 lowercase hexadecimal characters; no other algorithm or representation is accepted. On top of that, each manifest descriptor carries a SHA-256 checksum — exactly 64 lowercase hexadecimal characters — and the byte size of the complete pack. Both are validated before a downloaded pack is imported, and a mismatch is a storage-integrity error: the server refuses the import and does not advance or materialise corrupt state.

The downloaded pack-byte cache is keyed by that SHA-256, never by an untrusted S3 key, and a cache hit still has to match the expected size and pass a fresh SHA-256 check. That is what makes the optional shared pack cache safe to share between agents: no entry is trusted for its location or its writer.

Checkpoints and stable-prefix compaction

After the active tail reaches --checkpoint-packs (256 by default) increments, one checkpoint effort is scheduled for that observed manifest. Retained tails do not count toward the threshold. The worker takes the oldest increments up to the threshold as its stable prefix, and the generation of the last selected increment becomes the cutoff.

Compaction does not block normal writers. When the checkpoint pack is ready, the worker reads the latest manifest, and if that manifest still contains its compacted prefix, replaces only that prefix and keeps every later increment. The proposal generation is the latest generation plus one; the new checkpoint’s through-generation is the cutoff, and its head and publication ID are those of the final compacted increment. The final manifest replacement is the ordinary ETag compare-and-swap, so a lost race costs one more small manifest rewrite, never another checkpoint build.

The worker may reuse its pack only while the latest active descriptor chain still contains that exact prefix. A normal commit preserves it; a competing checkpoint at or beyond the same cutoff removes it, and then the proposal is discarded. Competing checkpoint workers are therefore safe: at most one manifest replacement wins, and the loser’s pack is simply an unreferenced object. A checkpoint failure never changes the result of an already accepted commit.

Retention and cleanup

The manifest keeps the active checkpoint generation and, by default, one previous generation; --retained-checkpoints configures the count, where 0 retains only the active generation. Each retained entry holds its checkpoint descriptor, its complete ordered increment tail through the cutoff that replaced it, and its accepted head — enough to reconstruct the exact state the newer checkpoint replaced, so a stale reader can restart.

Physical objects are removed on a best-effort basis, only after a later checkpoint’s manifest write has succeeded:

C0 becomes C1  -> retain C0 and C1 storage
C1 becomes C2  -> retain C1 and C2, then clean C0 storage

The active and retained manifest data are the garbage-collection roots. Cleanup lists only the packs/checkpoints/ and packs/increments/ namespaces, parses the generation out of each valid protocol key, ignores malformed keys, and considers only keys at or before the cutoff of the checkpoint that triggered it. Before each delete batch it rereads and validates current and rebuilds the live key set; it sends at most 1,000 keys per delete request, follows every listing continuation token, and records a per-key delete error to retry on a later cleanup. The current key is never a deletion candidate.

Cleanup failure does not fail a commit or a checkpoint. If checkpoints do not succeed, cleanup does not run and old proposals simply remain stored. An orphan proposal after the current cutoff is left alone until a later cutoff makes its generation eligible. And a reader never depends on a download timeout for safety: a stale reader that finds a removed pack rereads current and restarts.

What is history and what is not

slivingdoc promises current-state durability. It does not promise historical recovery.

  • A checkpoint preserves the exact file state at one accepted head. It omits older commit ancestors, records that head as a shallow boundary, and validation permits only that declared gap.
  • Commit messages are retained in recent internal Git data only.
  • There are no branch, tag, ref, revision, checkout, or rollback APIs, and no public Git remote.

Warning: Bucket versioning, replication, object lock, lifecycle rules, and external backups are deployment recovery policies. They complement slivingdoc and are not prerequisites of the synchronisation algorithm — choose them for your own recovery requirements. See Guarantees and limits.

Next

  • S3 requirements — the permissions and the conditional-write guarantees a store must provide.
  • Guarantees and limits — the failure table and what remains the operator’s job.
  • Configuration — --checkpoint-packs, --retained-checkpoints, and the rest.

Last updated September 21, 2026

Type to search the documentation.