How it works
One notebook cycle in six steps — pull, edit, commit, race, conflict, checkpoint — and the Git and S3 machinery behind each of them.
slivingdoc gives many agents one shared directory of notes, and they read and write it with ordinary file tools. Between two calls there is no protocol at all. The product has two priorities: resolve concurrent file changes quickly, and store the current notebook durably in S3-compatible object storage.
It is not a source-control product. It uses Git data structures and Git merge behaviour internally, but it never exposes a Git repository, and the caller never receives a Git object ID, a pack name, an S3 key, or a local checkout path. Those are implementation details.
The four states
Every operation is a statement about four states, and the rest of this page uses their names.
| State | What it is |
|---|---|
| Local state (L) | The visible directory you own. Agents and humans edit it, and slivingdoc can rewrite it during calls. |
| Private state (P) | slivingdoc’s own Git data, object cache, accepted baseline, merge scratch state, and conflict metadata. |
| Accepted baseline (A) | The remote state recorded in P as the base for the unpublished changes now represented by L. |
| Remote state (R) | The accepted notebook state indexed by the S3 current manifest. Unreferenced objects are not part of it. |
L holds no .git directory. P holds the internal repository and the
baseline, and lives outside L — see
Share a directory with humans for where
each root is placed.
1. Pull
notes_pull writes the current notebook into your directory. It treats
every difference between the accepted baseline in P and what is on disk
in L as an unpublished local change, merges those changes onto R, and
rewrites L with the result.
The sequence is:
validate and lock L and P
-> validate and ingest text files from L
-> GET current and validate the manifest
-> download missing checkpoint or increment packs
-> validate and import packs into P
-> merge accepted baseline, L, and R
-> rewrite L; record R as the new baseline in P
Pack downloads can happen concurrently; state is reconstructed in the order the manifest records. Pull always materialises the full merge result, so every non-conflicting path holds clean merged content, and a conflicted path holds its marker content. Pull does not revert L.
If current does not exist, R is the canonical empty tree: valid local
files in L are kept as local additions, the empty tree is recorded as the
baseline, and no remote state is created. If P has no accepted baseline
at all, pull starts from that same empty tree, so existing files in L
merge with R rather than being overwritten.
2. Edit
There is no filesystem watcher. slivingdoc ingests and rewrites L only at operation boundaries, which is what keeps it clear of partial editor writes and atomic-rename patterns. The rule for the caller is simple: edit the files only between calls, and do not modify L while an operation for that path is running.
Everything below L is notebook state, with one exception: a file under a configured read-only path is taken from the accepted baseline instead of from L. There is no ignore file. See Restrict agents with path policies.
3. Commit
notes_commit publishes your changes and incorporates concurrent,
non-conflicting changes by other writers. It needs a non-blank message
and an accepted baseline in P, so the first commit for a directory must
follow a pull.
read and validate text files from L
-> reject complete conflict-marker blocks
-> check read-only paths; reset and refuse on a violation
-> build the local tree and read R with its ETag
-> merge(A, L, R)
-> create the commit and an incremental pack
-> upload the immutable pack
-> PUT current with If-Match
The order of the last three steps is load-bearing. Pack bytes are always
written before the manifest that references them, and only the manifest
update accepts a proposal: uploading a pack publishes nothing. Once
acceptance is proved, commit rewrites L to the exact accepted merged tree
and records that tree and head as the new baseline in P before it returns
OK. If the merged result already equals R, commit does that local
synchronisation and makes no remote request at all — no publication ID,
no pack, no commit, no conditional write.
4. A lost race
Many writers can read the same manifest, merge, and upload packs at the same time. S3 accepts only one replacement for a given ETag:
current A, ETag e1
writer 1: A -> B, PUT If-Match e1 ---- accepted ----> current B, ETag e2
writer 2: A -> C, PUT If-Match e1 ---- rejected
writer 2: merge C with B
writer 2: B -> D, PUT If-Match e2 ---- accepted ----> current D
A failed comparison is expected contention, not a storage failure. The losing writer downloads the increments it is missing, merges against the new head, waits with bounded randomised backoff, and tries again with a fresh publication ID, target generation, pack key, commit, and pack. It never republishes the losing attempt’s pack against a later generation.
There is no lock object, no lease, no renewal loop, and no clock-based
expiry anywhere in the protocol. Only the small current object orders
successful publications.
Note: A lost race is invisible to the caller. Retries are bounded by
--commit-retries; only exhausting that bound surfaces, asREMOTE_BUSY. See Configuration.
5. A conflict
A conflict is the one case slivingdoc refuses to decide for you: your change and the accepted remote state touch the same lines of the same file, or the same path on one side is a file and on the other a directory.
The merge is a three-tree merge over the accepted baseline A, the local
tree L, and the remote tree R, performed by libgit2. It computes the
local change as A -> L, the remote change as A -> R, and the result
as merge(A, L, R). There is no history merge base: the three trees are
passed explicitly, with the merge labels local and remote, which are
exactly the labels you then see in the markers. Conflicted files are
identified from the merge index, not from a text scan of the directory.
The operation does not update current. It writes every non-conflicting
result and every conflict file into the visible directory and returns the
affected paths with their one-based, inclusive marker line ranges. You
resolve the files with ordinary file tools and commit again; the resolved
directory then becomes your local intent, and if remote state moved in
the meantime, the server merges once more. No Git command is involved at
any point. Resolve conflicts walks through one.
6. Checkpoints
Every normal commit uploads one small incremental pack, which is what makes commits cheap. An unbounded chain of increments would make a cold start slow and request-heavy, so the server periodically compacts a stable prefix of accepted increments into one checkpoint pack that reconstructs the complete state on its own.
before checkpoint
C0 -> I1 -> I2 -> ... -> I256 -> I257 -> I258
\_______ compacted ______/ \__ tail __/
after checkpoint
C1(at I256) -> I257 -> I258
A checkpoint is built for an immutable prefix while newer commits continue, so it never blocks writers, and its failure never changes an already accepted commit. A checkpoint preserves current file state, not permanent history: it keeps the checkpoint head commit and its complete tree, and omits older ancestors. Storage model covers compaction, retention, and cleanup in detail.
The Git sequence, on the inside
Letting every agent write at once would work, but the writers would be overrun by race conditions. So slivingdoc has Git built in, through libgit2, and effectively does this for you:
git pull- (potential conflict resolution locally)
git add .git commit -m "<agent message>"git push- (potential conflict resolution locally)
All of it runs locally, inside a private mirror of the notes directory, which leaves a streamlined Git sequence with no Git server and no network transport in libgit2 at all. The process never invokes a Git command, and you do not need Git, libgit2, a C compiler, or CGo on your machine.
That works because of two deliberate compromises. First, the visible
notes directory is liable to be rewritten on notes_pull: precedence
goes to the remote state, and conflicts are left as markers in the files.
Second, the system works for clean UTF-8 text only.
Commit history is an internal aid for recent ancestry and pack creation. It is not the durable product contract, and V1 does not promise permanent message or commit-history retention.
Why Git and S3 together
Each half has one responsibility, and neither does the other’s job:
| Component | Responsibility |
|---|---|
| libgit2 | Build trees and commits, create and import packs, and merge three file trees. |
| S3 | Store immutable bytes, expose the accepted manifest, and enforce conditional publication. |
| slivingdoc | Apply product policy, manage paths, retry contention, show conflicts, and create checkpoints. |
The result uses mature Git merge behaviour without operating a Git
server, and avoids both a mounted object-store filesystem and a separate
coordination database. Most work happens concurrently; only the final
conditional replacement of the small current object orders successful
publications.
Next
- Storage model — what is in the bucket and what the manifest guarantees.
- Guarantees and limits — what slivingdoc promises, and what stays your job.
- MCP tools — the exact inputs and results of the two operations.