slivingdoc — agents' s3 living documents

6 min read Original article ↗

agents' s3 living documents

One notebook, many agents — and their humans. Two operations:

The concurrency guarantees given by slivingdoc allows for agents to seamlessly share context regardless of where, and when, they are run. Only restriction being access to a S3-compatible bucket.

Leave a note for your agent, or listen in to your agents' notes.

Many agents, commit concurrently; slivingdoc orders them into one bucket
concurrent in  →  serialized out

pull

Pull the notebook into a directory

pull mcp: notes_pull() OK cli: slivingdoc pull OK reads the accepted state from the bucket — one small manifest called current, plus the immutable packs it references — and writes the notebook into your directory as ordinary UTF-8 text files.

No path needed: every server owns one directory and names it in each result. That directory is a checkout, not a place inside the notebook — it mirrors the whole notebook and remembers its own baseline, what it last saw. Unpublished local edits are merged with the accepted state, never overwritten.

The caller never sees Git object IDs, pack names, or S3 keys. The directory is the whole interface.

notes_pull asks S3 for the manifest, streams packs from remote, merges, and writes files to local
GET current → packs from remote → merge → files to local

edit

Edit with ordinary file tools

Between calls there is no protocol at all. Agents — as many as you like — read and write the files with the tools they already have: a text editor, sed, an LLM's file tools.

The notebook is directories and UTF-8 text files — binary files are rejected, never mangled. Bytes and line endings are preserved. slivingdoc does nothing until the next call.

As such, humans can work in the same directory: open the notebook in your editor and write alongside your agents. Its perfectly possible to use slivingdoc as a agent-free distributed notebook as well.

An agent and a human type in the local notes at the same time; nothing syncs until the next call
no daemon · no hidden sync — files move only on pull and commit

commit

Commit publishes your changes

commit mcp: notes_commit(message) OK cli: slivingdoc commit -m "note" OK merges your files with the latest accepted state, packs the result, and uploads the pack. Packs are immutable: uploading one publishes nothing.

Your change is the delta since the checkout's baseline — so a directory that was never pulled is refused with INVALID_REQUEST before any network work. Pull once first: commit publishes intent, not a guess.

Publication is a single conditional replace of the small current manifest — If-Match on the ETag the writer observed. The bucket is the durability boundary; everything local is a rebuildable cache.

Commit reads the accepted state, merges, uploads a proposal pack, and the conditional swap accepts it
a pack is only a proposal until the conditional swap

race

Concurrent writers cannot clobber each other

Two agents commit at once. Both observed current at ETag e1; S3 accepts exactly one replacement for it. The loser gets a precondition failure — expected contention, not an error.

The losing writer downloads what it missed, merges against the new head, and retries with a fresh proposal. No lock object, no lease, no clock. Accepted state is never silently overwritten.

Two slivingdoc writers race on If-Match; the loser merges and retries, and overlapping lines return to the caller
optimistic publication: merge and retry, never overwrite

conflict

Conflicts are just text

When your change and an accepted remote change touch the same lines, the call returns CONTENT_CONFLICT and writes standard conflict markers into the affected files. The structured error names every file and marker line range.

Resolve with the same file tools you edit with: keep the text you want, delete the marker lines, call notes_commit again. A complete marker block is never accepted into the notebook.

A conflict appears as markers in the file and is resolved by editing
markers in, edits out — no Git commands involved

checkpoint

Checkpoints keep cold starts fast

Every commit uploads one small incremental pack. Left alone, that chain would make a fresh server download thousands of objects. So after a configurable count (1,024 by default), slivingdoc compacts the stable prefix into one complete-state checkpoint.

A new reader needs one checkpoint plus the short tail after it. Checkpoints never block writers, and a failed checkpoint never touches accepted state.

Increments stream in at a constant tempo while the stable prefix compacts into a new checkpoint generation; the previous generation is retained and the oldest is cleaned
compaction is async — the increment stream never pauses

Get started

Node 22+ and an S3-compatible bucket. Works in any MCP host — or with none at all: the same binary is a CLI.

1

Point it at a bucket

That is the whole configuration. AWS S3, Tigris, MinIO, or anything S3-compatible that supports conditional writes. Credentials come from the normal AWS chain.

2

Add it to your MCP host

The npm launcher downloads the right native binary for your platform, verifies its SHA-256, and runs it. No Git, no toolchain.

3

Let agents pull and commit

Two tools, no arguments to coordinate, plain files, OK on success. Conflicts come back as editable text, never as a stuck state.

Raw MCP configuration for any compatible host
{
  "mcpServers": {
    "slivingdoc": {
      "command": "npx",
      "args": [
        "-y", "slivingdoc", "serve",
        "--bucket", "my-notes"
      ],
      "env": {
        "AWS_ACCESS_KEY_ID": "<your-access-key-id>",
        "AWS_SECRET_ACCESS_KEY": "<your-secret-access-key>",
        "AWS_REGION": "us-east-1"
      }
    }
  }
}

Each server takes its own scratch directory and discards it on exit — the notebook lives in the bucket. Pass --workspace-root to pin a fixed directory that humans and agents share instead. Prefer AWS_PROFILE over pasted keys when ~/.aws is configured, and for a non-AWS store add AWS_ENDPOINT_URL_S3.

claude mcp add slivingdoc \
  --env AWS_ACCESS_KEY_ID=\
<your-access-key-id> \
  --env AWS_SECRET_ACCESS_KEY=\
<your-secret-access-key> \
  -- npx -y slivingdoc serve \
  --bucket my-notes

Add --scope project to write a shared .mcp.json instead of your user config.

codex mcp add slivingdoc \
  --env AWS_ACCESS_KEY_ID=\
<your-access-key-id> \
  --env AWS_SECRET_ACCESS_KEY=\
<your-secret-access-key> \
  -- npx -y slivingdoc serve \
  --bucket my-notes

Stored in ~/.codex/config.toml under [mcp_servers.slivingdoc]. Verify with /mcp.

gemini mcp add -s user \
  -e AWS_ACCESS_KEY_ID=\
<your-access-key-id> \
  -e AWS_SECRET_ACCESS_KEY=\
<your-secret-access-key> \
  slivingdoc -- npx -y slivingdoc serve \
  --bucket my-notes

Written to ~/.gemini/settings.json; drop -s user for a per-project entry.

{
  "mcp": {
    "slivingdoc": {
      "type": "local",
      "command": [
        "npx", "-y", "slivingdoc", "serve",
        "--bucket", "my-notes"
      ],
      "enabled": true,
      "environment": {
        "AWS_ACCESS_KEY_ID": "<your-access-key-id>",
        "AWS_SECRET_ACCESS_KEY": "<your-secret-access-key>"
      }
    }
  }
}

Goes in opencode.json at the project root, or globally in ~/.config/opencode/.

pi install npm:pi-mcp-adapter

pi has no built-in MCP. The adapter reads the standard .mcp.json — paste the spec above into your project's .mcp.json and restart pi.

npx -y slivingdoc pull notes --bucket my-notes
# edit UTF-8 text files under notes/
npx -y slivingdoc commit notes --bucket my-notes -m "meeting summary"

The same two operations, driven by hand — humans and cron jobs share the notebook without an MCP host. Same flags and environment as serve. The path is optional and defaults to the working directory; a relative one resolves against it.

No bucket yet? Tigris is plug-and-play: a free tier, one global endpoint, and the conditional writes slivingdoc's startup probe requires. Set AWS_ENDPOINT_URL_S3 to https://t3.storage.dev and AWS_REGION to auto. For offline evaluation, the SeaweedFS example runs a local bucket in one docker compose up.