# 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.

Canonical URL: https://www.slivingdoc.dev/docs/concepts/how-it-works/ · Version: 0.1 · Updated: 2026-09-21

Full site index: https://www.slivingdoc.dev/llms.txt

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](/docs/guides/shared-directory/) for where
each root is placed.

## 1. Pull

![A pull downloads the accepted state from the bucket and writes it into the visible directory](/diagrams/pull.svg)

`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:

```text
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

![Between calls, agents and humans edit the files in the directory with ordinary tools](/diagrams/edit.svg)

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](/docs/guides/path-policies/).

## 3. Commit

![A commit uploads an immutable pack and then conditionally replaces the current manifest](/diagrams/commit.svg)

`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.

```text
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

![Two writers publish from the same observed state; only one conditional write is accepted](/diagrams/race.svg)

Many writers can read the same manifest, merge, and upload packs at the
same time. S3 accepts only one replacement for a given ETag:

```text
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, as `REMOTE_BUSY`.
> See [Configuration](/docs/reference/configuration/).

## 5. A conflict

![Two writers change the same lines; the merge writes conflict markers into the file](/diagrams/conflict.svg)

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](/docs/guides/conflicts/) walks through one.

## 6. Checkpoints

![A checkpoint compacts a stable prefix of increments into one complete-state pack](/diagrams/checkpoint.svg)

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.

```text
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](/docs/concepts/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](https://github.com/libgit2/libgit2), and effectively does this
for you:

1. `git pull`
2. (potential conflict resolution locally)
3. `git add .`
4. `git commit -m "<agent message>"`
5. `git push`
6. (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](/docs/concepts/storage-model/) — what is in the bucket
  and what the manifest guarantees.
- [Guarantees and limits](/docs/concepts/guarantees/) — what slivingdoc
  promises, and what stays your job.
- [MCP tools](/docs/reference/mcp-tools/) — the exact inputs and results
  of the two operations.
