Skip to main content

Worktree snapshots

A session or workflow run records the state of each Git checkout it changes, so it can resume on another machine, or after a worktree was deleted or reset, with the repository content it was working on.

What a snapshot holds

For each repository root a session or run touches, a snapshot records:
  • root: the checkout’s path on the recording machine (informational);
  • remotes: fetch URLs of the configured remotes, with any credentials in the URL removed;
  • branch and head: the current branch (or null when detached) and the HEAD commit;
  • tree: the Git tree of the whole working copy, built through a private index, so your index, branch and files are never touched;
  • a patch chain: a binary delta against head, then incremental deltas against earlier snapshots, restarting from head every snapshots.fullEvery snapshots or when HEAD moves;
  • a bundle of commits reachable from HEAD but from no remote-tracking ref, so unpushed work travels too;
  • omitted: what was left out and why;
  • filters: conversion settings on the recording machine (core.autocrlf, core.eol, filter drivers such as Git LFS that .gitattributes names);
  • envHash: a SHA-256 over the content of lockfiles and toolchain files at the repository root (package-lock.json, Cargo.lock, .devcontainer/devcontainer.json, .nvmrc, and similar), so you can tell when dependencies need reinstalling.
Patches and bundles are stored once, by content hash, in the same storage as the session or run (the snapshot_blobs table of local SQLite or a database adapter). Machines that share a database see the same blobs and need nothing else. What “the same tree” means: a restore reproduces the repository content as Git stores it: the restored worktree’s Git tree id equals the recorded tree, and every file’s blob matches. It does not reproduce host-specific worktree encodings. A source checkout with core.autocrlf=true and CRLF files restores with LF files and the same Git content, and the restore warns about the source’s conversion settings.

When snapshots are taken

  • Sessions: at the end of each turn that called bash, edit, write, powershell, or an extension tool that declares writesFiles: true. The session’s directory and the directories of the files edit and write targeted are captured.
  • Workflows: after each completed stage and each completed ctx.tool call, for the stage’s working directory and every worktree the run binds. Pooled workers snapshot their own worktree.
A capture that finds HEAD, the recomputed tree, and the set of left-out files all unchanged records a reference to the previous snapshot instead of a new one. A newly left-out file, such as a new .env.local, records a new snapshot that lists it in omitted, so a restore warns about it. Each checkout’s captures share a private Git index under ~/.atomic/agent/worktree-snapshots/capture-index/, so after the first capture, unchanged files are not read again; this matters most in fresh clones such as CI checkouts. It never changes what a snapshot records. Deleting the directory is safe: the next capture rebuilds it from your index, and a capture that cannot use it, for example because it is damaged, captures without it. Snapshot commands use the Git Atomic resolves, including a portable copy it provisions when Git is not installed. If Git cannot be provided, or a directory is not inside a Git repository, nothing is recorded for it and behavior is exactly as without snapshots. In-memory sessions record nothing. A persisted store that cannot hold blobs (JSONL session directories, storage adapters without a database) records nothing and reports blob-store-unsupported once. A store that fails while a turn stores its snapshot, for example a database server that cannot be reached, does not fail the turn: the turn records nothing and blob-store-failed is reported once. Patches and bundles over 1 MiB are stored as rows of at most 1 MiB, so every file up to snapshots.maxFileBytes can be captured on every backend, a libSQL server included. When a file or directory a capture has to read cannot be read, for example because of a permission error or a symbolic link that loops, that checkout’s capture records nothing and reports unreadable-path naming the path. The turn or stage carries on. Make the path readable, or list it in .atomic/snapshot-ignore, to snapshot that checkout again.

What is left out

  • Files ignored by .gitignore, .git/info/exclude, or your global excludes file. Build outputs and dependencies are rebuilt, never captured. A file you staged despite an ignore rule (git add -f) is captured like any other staged file.
  • Paths matching .atomic/snapshot-ignore at the repository root. Its rules match exactly as a .gitignore at the root would, and are case-insensitive only when the repository sets core.ignorecase.
  • Paths on the built-in deny list: .env*, *.pem, *.key, *.p12, *.pfx, SSH key files: any id_* file without an extension, and any file named after an SSH key type (id_rsa, id_ed25519, id_ecdsa, id_dsa, id_mldsa, id_xmss) whatever its suffix, such as id_rsa.bak, except .pub public keys and certificates. Other id_* files with an extension, such as id_utils.ts, are kept. The secret extensions apply to every name, so id_server.pem or id_rsa.pub.key is always left out, credentials, credentials.json, .netrc, _netrc, .git-credentials, .pgpass.
  • Untracked files when snapshots.includeUntracked is false.
  • Files over snapshots.maxFileBytes, and files beyond snapshots.maxSnapshotBytes in total.
  • Untracked repositories inside the checkout, such as tools/ after git init tools. omitted lists the directory as nested-repository.
  • Work inside submodules. The snapshot records only the commit each submodule has checked out, and a restore does not check submodules out. omitted names a submodule that has uncommitted changes or untracked files (submodule-changes), or whose checked-out commit is on none of its remote-tracking branches (unpushed-submodule-commit). Commit and push that work in the submodule to keep it. A submodule that could not be checked within 10 seconds is listed as submodule-unchecked.
Tracked files that are denied or ignored keep their HEAD content in the snapshot; changes you made to them are listed in omitted. Deny-list guarantee: no deny-listed bytes are captured unless they are already in a commit reachable from a remote-tracking ref. If an unpushed commit adds or modifies a deny-listed path, the bundle of unpushed commits is left out and omitted names those commits and paths. Restoring that snapshot then fails until you push them. A bundle larger than snapshots.maxSnapshotBytes is left out the same way.

Settings

The SDK takes the same fields as createAgentSession({ snapshots }) and workflow RunOpts.snapshots; they override settings. Invalid values fail with WorkspaceConfigError before anything runs.

Resuming

When a session resumes, Atomic checks every checkout the session recorded a snapshot for, not only the one that contains its working directory:
  • If the tree matches and HEAD is the recorded commit or a descendant of it, for example after you committed the recorded edits, the session resumes in place.
  • If the checkout that contains the session’s working directory changed since its snapshot but is still the one the session worked in on this machine (its repository holds the snapshot’s commit or the session’s snapshot ref, or Atomic restored it here), the session continues in it as it is, without a notice. Another clone at the same path has neither, so it is restored as described below. Nothing is restored for it. If looking for that checkout fails, the resume carries on with the restore described next.
  • A session that an earlier resume moved into a restored worktree, and that has not worked in it since, returns to the checkout it was restored from when that checkout is on this machine again, with a notice that says whether it still matches the snapshot.
  • If another checkout drifted, or the checkout that contains the working directory does not exist on this machine, belongs to another clone, or is no longer a repository with commits, Atomic restores its snapshot automatically into a fresh worktree under ~/.atomic/agent/worktree-snapshots/worktrees/, prints one line per restored checkout, and continues the session there. Your own checkout is never written to. The lines, and any restore warnings, appear at startup and after /resume or /import in the TUI, and the session records the warnings with each restore.
  • A checkout other than the one that contains the session’s working directory never stops the resume. One the session worked in on this machine (a sibling worktree, for example) that changed since its snapshot continues as it is. One that is missing, or that another clone holds at its path, is restored into a fresh worktree when Git is available. One that cannot be checked or restored is left out, and a warning names its path, its snapshot and why. Each restored worktree then gets its blueprints built, and a blueprint step that fails never leaves a checkout out or removes its worktree: it is a warning, and the resume continues.
  • A resume always opens the session with its full conversation. When a checkout’s snapshot cannot be restored exactly (its unpushed commits touched denied paths or were too large to record, a remote is unreachable, a blob is missing or damaged, or a change set does not apply), Atomic restores the best state this machine can reach into a fresh worktree, trying in order:
    1. the recorded HEAD with the snapshot’s change sets that apply, each one whole or not at all, up to the first that does not. When this machine’s checkout filters change the files it writes, the worktree is kept as written, and the notice names the files that differ;
    2. else the commit closest to the recorded HEAD that it can fetch from the recorded remotes: the pushed commit the snapshot’s unpushed commits were built on, when the bundle is readable, then the commits of every earlier snapshot of the same checkout, nearest first, then the tip of the recorded branch (the remote’s default branch for a detached HEAD);
    3. else nothing: the session continues in its recorded working directory, used as it is and never written to, or, when that directory is not on this machine, in the directory the resume started from. Git being unavailable on this machine, and a session that refers to a snapshot it holds no record of, end here too.
    Each such checkout gets one notice, at startup and after /resume or /import, that says what was restored, what is missing (commits the restored history does not already hold, change sets and the files they touch) and the next step, for example Restored /work/app into ~/.atomic/agent/worktree-snapshots/worktrees/… at 3f2a9c1d7b04, the tip of branch main on its remote, instead of the recorded commit 8e0b51c2a7d6, the closest commit available here. Not restored: 1 commit (8e0b51c2a7d6) and the snapshot's uncommitted changes of snapshot 5c1d…. Push commit 8e0b51c2a7d6… (with its history) to a recorded remote, then retry. A resume that restores everything shows no such notice. The session records the outcome on the restore entry (restore.degraded) or, when nothing was restored, as an unrestored entry, and /session snapshot shows it under the checkout. A later snapshot in a partly restored worktree records the tree and commit it holds, never the original snapshot’s, so it never presents a partial restore as the original state; the original snapshot stays the checkout’s restore target, as Snapshots restored only partly describes. Only three things refuse a resume: a session that cannot be read, a snapshot record that fails the security checks below (invalid-snapshot, for example a remote with credentials or a Git remote helper), and a restore target that holds other files (unsafe-target). Restores never write into an existing checkout, and a checkout with uncommitted changes is used only when you choose it. A refused resume fails with that error; in the TUI, a /resume or /import that fails this way shows the error and keeps the current session.
A working directory reached through a symbolic link is matched by the directory the link points at, because snapshot roots are Git’s physical top-level directories.

Snapshots restored only partly

A snapshot that a resume restored only partly stays the checkout’s restore target, and collection keeps its content, its whole patch chain included, until one of three things happens: a later resume restores it fully, you release it with discardDegradedRestore (see SDK), or 30 days (DEGRADED_RESTORE_RETENTION_DAYS) have passed since the partial restore. Each turn’s snapshot of the partly restored worktree records what that worktree holds and names the original in degradedFrom, so it never replaces the original as the snapshot a resume restores, and /session snapshot keeps showing the partial-restore notice. A later resume first tries to restore the original snapshot exactly, for example on a machine that can fetch commits you pushed since:
  • When it can, and the session changed files in the partly restored worktree, the resume applies those changes on top: the difference from what the partial restore wrote to the worktree’s latest snapshot, whole or not at all. Applied changes get an info notice and are recorded in a new snapshot right away. Changes that do not apply leave the restored worktree as the original snapshot; a commit of the worktree’s repository, under refs/atomic/degraded-work/<snapshot>, keeps them, and a warning gives the git diff … | git apply --3way command that applies them. Changes that cannot be read on this machine, for example because their own commits were never pushed, are reported and stay in the partly restored worktree where they were made, and the original snapshot is then no longer pinned.
  • Applied changes end the pin only once their new snapshot is recorded. Until then the restore entry names the work’s snapshot in degradedFrom, so collection keeps it and a resume on another machine restores the original and applies the work again. If that capture fails (a Git error, a crash, Ctrl-C), a warning says the work is not recorded yet, and the next turn’s capture, or the next resume on the machine whose worktree holds it, records it.
  • Changes that do not apply stay kept as the work’s snapshot in the session’s storage, not only as the ref on this machine: the restore entry and every later snapshot of the checkout name it in degradedFrom, so collection keeps it until discardDegradedRestore releases it, or 30 days have passed since that restore. The checkout continues from its latest snapshot; while that is still the original snapshot, a resume on another machine keeps the work as the same kind of commit there.
  • When it cannot, the resume restores the partly restored worktree’s latest snapshot instead, so the work done there is not lost, and a warning says the original still cannot be restored. The original stays pinned.
After the 30 days, the next resume shows a warning that the original is no longer kept, the partly restored worktree’s latest snapshot becomes the checkout’s restore target, and the next turn records it without degradedFrom. Collection counts the same 30 days on the database’s clock, from the first snapshot that named the original, so it never releases it earlier. Atomic versions before 0.9.88 ignore degradedFrom and treat the partly restored worktree’s latest snapshot as the checkout’s latest.

Restored worktrees

A session resume restores each checkout into a worktree at a stable path, ~/.atomic/agent/worktree-snapshots/worktrees/<repository>-<n>, where <repository> names the repository by its remotes and <n> is the lowest number free on this machine. The session holds that worktree while it is open and releases it when it closes (quitting, /new, or /resume of another session), or when its process stops. The record of each worktree lives in ~/.atomic/agent/worktree-snapshots/pool/. Only checkouts of one repository share these worktrees: checkouts whose remotes name the same repository, with a relative-path remote such as ../upstream taken as the absolute path it resolves to from the checkout. A checkout with no remote is never pooled, because nothing recorded tells its repository apart from another remote-less one, and pooling it could hand one project’s ignored files, such as secrets/ or node_modules/, to another. Its resume restores into a new worktree each time, as a workflow run’s restore does, and atomic blueprint prebuild refuses it. Add a remote to pool its worktrees. A reused worktree must also belong to the resuming repository’s object store; Atomic leaves any other as it is. Worktrees that earlier versions pooled for remote-less checkouts are no longer reused. A worktree belongs to the session it was restored for, also after that session closes, so resuming the session again continues in it as it left it. A session never continues in a worktree that another session now holds or owns: it gets a fresh worktree instead, as if its old one were gone. Worktrees that workflow runs restore, that the SDK restores into a directory you chose, and that earlier versions of Atomic restored keep their own paths and are never part of this. A session holds its worktree through a lease that its process refreshes every 10 seconds. A process that stops refreshing it, because it was suspended, the machine slept, or the host was overloaded, keeps its worktree for 90 seconds after its last refresh: 30 seconds before the lease is stale, and a 60-second grace period after that. Only then can a session on another machine sharing the pool reuse or remove the worktree. Machines sharing a pool, as on a network filesystem, may have clocks that disagree. Atomic measures every lease’s age by the clock of the filesystem that holds the pool: each process finds how far that clock runs from its own by writing a short-lived .clock-*.probe file next to the lease, stamps its refreshes in the filesystem’s clock, and judges other leases by it. A session on a machine whose clock runs minutes behind keeps its worktree, and one on a machine whose clock runs days ahead loses it 90 seconds after its last refresh, like any other. The per-worktree and per-repository locks, the restore crash-record locks and the seed locks are judged the same way, each stale 30 seconds after its last refresh. On the same machine, a worktree whose session process still runs is never reused or removed, however long it was suspended; once that process ends, the 90-second rule applies. On Linux the lease names when its process started, so a later process that got the same process ID, as after a reboot or a container restart, never keeps the worktree. Where that cannot be read, as on macOS and Windows, a process with the lease’s ID keeps the worktree for at most 24 hours without a refresh. Reusing, removing and taking a worktree again happen under a per-worktree lock. Each change checks that the process still holds that lock and that the worktree’s record is the one it checked, and removing a worktree first takes its record and moves it aside, so a process suspended long enough to lose the lock changes nothing another session holds when it continues. Before it records a snapshot, and before a resume continues in a worktree, a session checks that it still holds it. When another session took the worktree meanwhile, the session records no snapshot of it, and its warning pooled-worktree-lost says to stop changing files there. Its next resume restores the session’s latest snapshot into a fresh worktree. A session that cannot take the lease of its worktree when it resumes restores fresh in the same way, and never continues in a worktree it does not hold. Before restoring into a new worktree, a resume reuses an idle worktree of the same repository when it can: one no open session holds and that is still exactly as its session’s latest snapshot recorded it, with nothing snapshots leave out, such as an untracked file or an ignored .env. Atomic resets its tracked files to the snapshot being resumed and keeps its ignored files, such as node_modules, target or .venv, so the blueprint’s maintenance steps have little to rebuild. Worktrees last restored for the same environment are tried first, then the most recently released. The worktree then belongs to the resuming session; its previous session gets a fresh worktree on its next resume, with nothing lost, because the reused worktree held only what its snapshot recorded. A worktree with unrecorded changes is never reused. When no worktree can be reused, or the reset fails or does not verify, the resume restores into a new worktree as before. A resume interrupted while resetting a worktree leaves it marked; it is never reused, and Atomic removes it on a later restore. /session snapshot lists a reused worktree as (claimed). When a resume restores a worktree and more than snapshots.pool.maxIdlePerRepository worktrees of that repository are idle (no open session holds them), Atomic removes the oldest idle ones that are still exactly as their session’s latest snapshot recorded them. An idle worktree with changes no snapshot recorded is kept, and so is one holding files snapshots leave out, such as an untracked file or an ignored .env; other ignored files, such as dependencies and build output, are removed with the worktree. If the pool cannot be used, for example because its directory cannot be written, the resume restores into a new worktree as before. Restored worktrees share one object store per repository on each host (~/.atomic/agent/worktree-snapshots/repositories/), so many restores of one base cost about one base plus their deltas. A checkout with no remote has a store of its own, named after its path, which it shares with no other checkout. A binding restored without the snapshot that names its checkout cannot tell which store is its, so restoring a remote-less binding without its snapshot fails with commits-unavailable. A restore that needs a commit the store lacks fetches only that commit (--depth=1), so a restored worktree starts with a shallow history: git log stops at the restored commit. Run git fetch --unshallow origin in the worktree when you need the rest. When a remote refuses a shallow fetch, or a restore needs older history (a snapshot’s unpushed commits built on an older commit, or an earlier commit to fall back to), Atomic fetches the full history instead. That store has the snapshot’s recorded remotes, the first named origin, the next origin-2, and so on, with the credential-free URLs the snapshot recorded, so git fetch and git push work in a restored worktree once you have access. Remotes are configured only when every recorded remote is a local path, an scp-like host:path, or an https, http, ssh, git or file URL without credentials; otherwise, and for a repository without remotes, the store has none. Commits a recorded remote is known to have, the ones the restore fetched and those the snapshot recorded as pushed, get remote-tracking refs under refs/remotes/atomic/pushed/. A snapshot captured in a restored worktree therefore records the same remotes and bundles only the commits that are really unpushed, so a session can move across any number of machines; commits that only a bundle carried stay unpushed. The session records the old-to-new root mapping (SessionManager.getWorktreeRootMappings()), and later resumes keep using the new root. A checkout matches its snapshot when every file of the recorded tree has the recorded content and HEAD is the recorded commit; verifyWorktree checks exactly that, while resume also accepts a HEAD that descends from the recorded commit over the same tree. Unchanged recorded files are compared even when this host’s excludes or size settings would leave them out; those settings only decide whether a file the snapshot never had counts as a change. A recorded file whose content changed and that is now left out, for example because it grew past snapshots.maxFileBytes or is listed in .atomic/snapshot-ignore, counts as drift. A checkout whose repository does not hold the recorded tree is treated as drifted and restored. A restore builds the worktree under a staging directory, verifies it there, and only then moves it to its final path, so a final path never holds an unverified worktree. If the process is killed mid-restore, the staging worktree stays behind with a crash record under ~/.atomic/agent/worktree-snapshots/restores/. The next restore or removeRestoredWorktree on that host removes it once the crash record’s lock is stale, 30 seconds after the process stopped. A leftover that cannot be removed is reported once as the stale-restore-retained warning naming its path, with kind saying what it is: a staging or nested worktree (remove it with removeRestoredWorktree or delete the directory), a crash record file that cannot be read (delete the file), or the crash-record directory itself, records-directory (make it readable or remove it). Snapshot records come from storage and from imported session files, so restore and verification check each record before running anything. Remotes must be local paths, scp-like host:path remotes, or https, http, ssh, git or file URLs without credentials (never a value starting with -, never control characters, never a Git remote helper such as ext::), and commits, trees, blob hashes, and the snapshot id must have the form Atomic writes. A record that does not fails with invalid-snapshot, which names the field but not its value. Restore fetches only over https, http, ssh, git, and local paths. Restore fetches never wait for input. On Linux and macOS they run without a terminal, so an unknown host key, a passphrase, or a credential prompt fails at once as commits-unavailable (reason: "not-fetched") instead of waiting. Unless you configured your own ssh command (GIT_SSH_COMMAND, GIT_SSH, or core.sshCommand), ssh runs with BatchMode=yes and a 15-second connect timeout. To fix such a failure, run git ls-remote <remote> once yourself to accept the host key or unlock the key, then resume again. When the repository uses Git LFS, a restore downloads the LFS objects of the snapshot’s files, including files edited or added since the recorded HEAD, from the recorded remotes, trying each in order until every LFS file has its content; a remote that is a local path is used as a file:// URL. The download never waits for input either. A file whose object no recorded remote has stays an LFS pointer, and the lfs-objects-unavailable warning lists exactly those files. Push the missing objects from the recording machine (git lfs push --all <remote> for committed files, git lfs push --object-id <remote> <oid> for an uncommitted edit), then restore again. Workflow runs follow the same rules at the point where execution resumes: each bound worktree is compared once with the last snapshot recorded for it, continues in place when only HEAD moved forward over the same tree, and is restored automatically into a fresh worktree when it drifted. A run, stage, or gitWorktreeDir path reached through a symbolic link maps to the restored worktree like the path it points at. A workflow run also always resumes. A checkout it cannot restore exactly goes through the same three steps as a session’s: the recorded HEAD with the change sets that apply, else the closest commit this machine can fetch (an earlier snapshot of the same checkout in the run counts), else nothing. Git being unavailable ends at the last step. With nothing restored, the run continues in its recorded directory, used as it is and never written to, or, when that directory is not on this machine, in the directory you resumed it from; the run keeps its recorded directory, so a later resume on a machine that has the checkout restores it. Each such checkout gets one warning in the run’s notices and one checkout: line in workflow status, and the run records it as worktreeRestoreOutcomes (the worktree mapping also carries degraded), so a later resume or a forkRun of the run knows the checkout is partial. A partly restored worktree stays reported until a later resume restores that checkout again. The run’s stages are told in their system prompt which checkouts hold less than recorded and what is missing. The same three refusals apply: a run or journal that cannot be read, invalid-snapshot, and unsafe-target. A pooled child on a worker host that must restore its bound tree does the same: when the tree cannot be restored exactly, the host restores the best state it can reach and runs the child there, and the child’s stages are told what is missing. When nothing can be restored, the host puts the child back for another host, as before. See Workflows operations.

Inspecting snapshots

/session snapshot lists the latest snapshot of each checkout in the session (branch, HEAD, tree, patch chain, bundled commits, omissions, conversion settings), showing a restored checkout once, under the worktree it was restored into, and compares it with the current checkout the way resume does, so it reports a match when only HEAD moved forward over the recorded tree, with a git diff --stat when the snapshot’s tree is available locally. It also shows how long the last turn’s capture of each checkout took (last capture took 0.4 s) and, after a resume that restored a checkout or built a blueprint, a Last resume block that says where that resume’s time went:
Each restored checkout gets one line with its restore phases in seconds: fetch (getting the snapshot’s commit, followed by full when it was fetched from a remote or bundle when it came from the snapshot’s bundle), reconstruct (applying the recorded changes), materialize (writing the files), LFS (downloading LFS objects), verify (checking the files against the snapshot) and publish (moving the worktree into place). A checkout restored with less than its snapshot says (degraded). The blueprint line lists each step that ran, marking a step that failed or timed-out. ready after counts from the start of the resume, or from the start of the process for atomic --session, until the checkouts were restored and the blueprints built; loading extensions and the model afterwards is not included. With resume.waitForBlueprint: "tools", ready after stops once the checkouts are restored, and the blueprint line appears when the build ends. Set ATOMIC_TIMING=1 to print the same lines to stderr when Atomic starts with a resume. The session keeps these numbers in a resume_timings entry, so they are also in exports and readable from the SDK.

Moving a session as a file

/export session.jsonl writes the session with the patch and bundle content of its snapshots inline, each once, and reports how much it includes. --import session.jsonl and /import check every blob against its hash and store it in the target storage before the session is saved. /share leaves snapshot content out of the gist.

Snapshot refs in your repository

Each snapshot is also a Git commit in the checkout’s repository. Atomic keeps the latest one each session recorded of a checkout reachable from a ref, refs/atomic/snapshots/<session-id>/<checkout>, so git gc and git maintenance do not prune it. This is how a resume recognises your checkout as the one the session worked in, even after you changed it. A session’s ref for a checkout:
  • moves to each new snapshot of that checkout, so it never keeps more than one snapshot commit alive per session and checkout;
  • is deleted when the session’s work moves to a worktree restored from that checkout, or back from it, and records a snapshot there;
  • is deleted from every checkout on this machine when you delete the session from the session picker, /resume, or the SDK’s SessionManager.deleteSession.
The refs are outside refs/heads and refs/tags, so git push, git push --all, git push --tags and a snapshot’s bundle never include them. git push --mirror pushes every ref and would push them, together with the snapshot commits, which contain your uncommitted changes except the files described in What is left out. They also appear in git for-each-ref and git log --all, as commits titled Atomic worktree snapshot. To exclude them from git log --all, use git log --exclude='refs/atomic/*' --all. A session deleted on another machine, or through SessionStore.delete in the SDK, leaves its refs here. Remove every Atomic snapshot ref of a repository with:
A session recorded by an older Atomic has no ref until its next turn records a snapshot of the checkout. Until then, if git gc pruned its snapshot commit, resuming it restores the checkout into a fresh worktree as for another clone.

Retention and collection

Snapshot content (patches and bundles of unpushed commits) is stored once per content hash in the database that holds the session or workflow journal. Atomic keeps the content these snapshots need and deletes the rest:
  • Sessions: for every branch of every session, internal workflow-stage sessions included, the latest snapshot of each checkout on that branch, when the session holds the snapshot’s record. A branch is a path from the session’s first entry to an entry nothing follows, so a session that went back to an earlier entry and continued keeps what each branch would restore when you return to it. A snapshot that every branch has a later snapshot of the same checkout for is released, except one a resume restored only partly, which the latest snapshot of its partly restored worktree keeps for up to 30 days (see Snapshots restored only partly). A checkout restored into a new worktree counts as the one it was restored from. Checkouts are told apart by the paths snapshots recorded, so one reached through a symbolic link under two names can keep one snapshot more than resume needs. A session with an entry whose parent is missing, or with two entries of one id, keeps every snapshot it records. Deleting a session releases its snapshots.
  • Workflow runs: every snapshot of a root run whose latest status is running, paused, failed or blocked, unless the run is marked not resumable. A run in any other state, such as completed, cancelled or killed, releases them. A failed or blocked run keeps them until it is resumed to completion or deleted. Deleting a run from history releases its snapshots. Run records themselves are never deleted by collection.
  • Workflow work on other hosts: with shared worker placement, the snapshot a root carries to its children’s worker hosts: the snapshot of the root’s own bound worktree while the root’s worker pool runs, and the snapshot each child carries for as long as the root run’s queued work exists, which is until the root run is deleted from history, whatever its status. See workflow operations.
Sessions and workflow runs that share one database share one blob table, and a collection keeps what either of them needs. When collection runs. With snapshots.autoCollect on, Atomic collects in the background when a session closes and when a root workflow run finishes. Each database is collected at most once per snapshots.collectIntervalSeconds after its last finished collection, across every Atomic process that uses it, measured on the database’s clock. Pooled workers’ runs never start a collection. Collection reads an index of snapshot references, not the full session or workflow history, and deletes content in small batches. If the process exits or is killed partway, a later trigger resumes after about two minutes. Committed batches stay committed; a batch cut short rolls back. Collection timers do not keep an otherwise idle process running. When the last session or workflow journal that Atomic opened on a database closes, Atomic aborts collection at its next batch boundary and closes the database before the awaited close resolves. Closing waits only for that interruption and database cleanup, never for the remaining collection. A remote connection or the batch already in progress may keep the process alive until cleanup finishes. While another process holds the database’s write lock, collection waits on unref’d timers and gives up after about two minutes. Databases written before the index existed. The first collection indexes what such a database already holds, once, in short steps in the background, and deletes nothing until that is done. A database that shares its blob table with sessions or runs written by an older Atomic is indexed the same way the next time it is collected. An index an older Atomic wrote gains the references it lacks, such as those of queued workflow work, the same way. Grace window. Content stored or reused less than snapshots.graceSeconds ago (one hour by default) is never deleted, so a capture that has stored its content but not yet recorded the snapshot is safe. A capture that builds on an earlier snapshot restarts that snapshot’s age before reusing its content; if the content is already gone, the capture records a full snapshot instead. What the rules above keep is kept whatever its age, so 0 only removes the protection of content nothing references yet, such as a capture’s content before its snapshot is recorded or its work is queued. A session or a workflow checkpoint never records a snapshot whose content is gone. If a collection deletes a capture’s content before the snapshot is recorded, Atomic stores the content again, or captures once more. If that fails too, nothing is recorded: the session warns with blobs-collected, and a workflow checkpoint is committed without the snapshot. The capture fails with this clear error instead of recording a snapshot that cannot be restored. If the warning repeats, raise snapshots.graceSeconds. Failures. A failed collection never affects the session or run that started it. It is recorded in the database’s snapshot_collection table (finished_at, deleted and error_code; the name carries the storage prefix, if any) and retried after the interval. A workflow run shows a warning notice carrying WorkspaceCollectionError. An SDK host gets a process warning, or its own onFailure handler. The atomic command line does not print process warnings, so there a failed collection is visible only in that table. If a warning or notice handler itself throws, that error is reported as a process warning and collection carries on. Disk space. SQLite reuses the space of deleted content for new content but does not shrink the file. Run VACUUM on the database while no Atomic process uses it to return the space to the operating system. To collect immediately, for example after deleting many sessions, call collectSnapshotGarbage from the SDK (see below).

SDK

  • captureWorktreeSnapshot(dir, { settings?, previous?, blobs?, signal? }) resolves captured, unchanged, disabled, or unavailable with the typed reason (GitUnavailableError or WorkspaceSnapshotError).
  • restoreWorktreeSnapshot(bindingOrSnapshot, { into?, blobs?, snapshots?, storeDir?, onWarning?, signal?, git? }) resolves to the path of a new, verified worktree. git ({ path, env }) runs that Git executable for the restore instead of the one snapshot operations resolve. into must be a new or empty directory. A binding is restored from a matching entry of snapshots, or from a host whose object store already holds its tree. Restore checks the target and every blob before writing, and removes what it created when it fails. onWarning receives omitted-content, filters, lfs-objects-unavailable (LFS files left as pointers), and stale-restore-retained (an interrupted earlier restore that could not be cleaned up, with its kind).
  • restoreDegradedWorktree(snapshot, failure, { blobs?, storeDir?, candidates?, onWarning?, signal?, git? }) restores the best state of snapshot this host can reach after restoreWorktree rejected with failure (a WorkspaceRestoreError that isDegradableRestoreFailure accepts: blob-missing, blob-corrupt, commits-unavailable, delta-failed or tree-mismatch), as a resume does: the recorded HEAD with the change sets that apply, else the closest commit it can fetch, trying the snapshot’s pushed base, then candidates (commit ids, nearest first), then the recorded branch’s tip. It resolves { path, degraded, warnings }, where degraded (WorktreeDegradedRestore) says what the worktree holds (stage, head, tree, basis) and what is missing (missingLinks, missingPaths, missingCommits, with the failure’s cause and hint). It rejects with failure when no commit is reachable, and with invalid-snapshot for a record that fails the security checks.
  • verifyWorktree(binding, dir, { settings?, signal? }) resolves to the checkout’s current binding when its HEAD and tree match, and rejects with WorkspaceDriftError otherwise.
  • restoreBestWorktree(snapshot, { blobs?, storeDir?, candidates?, signal?, git? }) runs the whole ladder a resume uses: restoreWorktree, then restoreDegradedWorktree when that failure allows it. It resolves { status: "restored", path, warnings, degraded? }, or { status: "unrestored", reason } when nothing could be restored (nothing is written then). It rejects only with invalid-snapshot, unsafe-target, a failure that is not a typed workspace or Git failure, or, after signal aborts, the failure the abort caused. unrestoredRecord(snapshot, reason, continuedIn, place) builds the WorktreeUnrestored record a resume writes for such a checkout.
  • discardDegradedRestore(sessionManager, { settings, root?, signal? }) gives up on the snapshots a resume restored only partly (Snapshots restored only partly): it records each partly restored worktree of the session, or only the one at root, as it is now, without degradedFrom, so that state becomes the checkout’s restore target and the next collection releases the original, or the conflicting work a full restore kept apart. It resolves with the roots it recorded, and rejects with blob-store-unsupported when the session’s storage holds no snapshot content, or with the reason a worktree could not be captured. degradedRestorePin(sessionManager, root) returns the pin of the checkout at root ({ snapshotId, since, base?, lapsed, work?, kept? }, where work names the work a full restore carried over that is kept until it is recorded, or, with kept, until it is discarded), or undefined when it has none. A restore mapping that followed a partial restore carries degradedWork ({ snapshotId, root, outcome: "applied" | "kept" | "unavailable", paths, base?, commit?, ref?, reason? }).
  • withWorktreeRestoreNotices(notices, work) runs work so that every session it opens gets a worktree_restore system prompt section listing notices, one per checkout restored with less than its snapshot or not at all; a workflow run uses it for its stages. currentWorktreeRestoreNotices() returns the notices in effect.
  • openSessionInWorktree(sessionManager, { settings, storeDir?, signal?, beforeRecord?, pendingMappings?, keepInPlace? }) checks every recorded checkout and restores drifted or missing ones without building blueprints. It resolves { sessionManager, notices } with the session reopened where it continues and one notice per restored checkout, followed by its warnings. beforeRecord(mappings) runs once every checkout is restored, with the mappings of the restored ones (none when nothing needed restoring), and before any restore is recorded in the session; when it rejects, nothing is recorded and the restored worktrees are removed. pendingMappings names checkouts already here that the session continues in: they count as recorded while deciding what to restore, and are recorded with the restores only when everything succeeded. keepInPlace is a set of checkout roots, after the session’s recorded restores, to continue in as they are without verifying them. A checkout whose exact restore fails is restored as restoreDegradedWorktree does, and recorded with restore.degraded; one of which nothing can be restored, or that cannotRestore (roots to the reason, such as a GitUnavailableError) names, is left out and recorded as unrestored, and the session continues in its recorded directory, or in fallbackCwd when that is not here. The result lists those in unrestored. With skipMissingRecords, a checkout whose latest snapshot the session holds no record of is left out and listed in missingRecords instead of failing with snapshot-not-found. Only invalid-snapshot, unsafe-target, and failures that are not about a checkout reject. resumeSessionWorktree does the same without reopening the session. For normal SDK resume, pass the stored session directly to createAgentSession({ sessionManager }), which checks restore prerequisites and builds blueprints as described in Resume on any machine.
  • isHeadOnlyAdvance(driftError, signal?) resolves true when a WorkspaceDriftError from verifyWorktree only moved HEAD forward over the recorded tree, the case resume continues in place.
  • canonicalizeWorktreePath(path) resolves the symbolic links in the existing part of a path and keeps the rest, the form snapshot roots are compared in.
  • removeRestoredWorktree(path, { storeDir?, onWarning?, signal? }) removes a worktree a restore created, for rolling back a restore of several checkouts. It refuses any other path, including a regular file.
  • collectSnapshotGarbage(blobs, { graceSeconds?, signal? }) deletes, from a SqlSnapshotBlobStore, every blob no retained snapshot needs (see Retention and collection) that was stored or reused at least graceSeconds ago (default DEFAULT_BLOB_GRACE_SECONDS, one hour, on the database’s clock), and resolves with the hashes it deleted. sqlSessionStore.snapshotBlobs and sqlRunJournal.snapshotBlobs are such stores. Any other store, or none, rejects with WorkspaceSnapshotError (blob-store-unsupported) before anything is read. The returned promise keeps your process running until the collection ends. An aborted collection rejects with the signal’s reason; what it deleted stays deleted.
  • scheduleSnapshotCollection(blobs, settings, { onFailure? }) starts the automatic collection that sessions and workflow runs use, when settings.autoCollect is set and one is due for the database. Invalid settings throw WorkspaceConfigError (invalid-setting) before any storage is touched. Otherwise it resolves with the outcome (collected, skipped with a reason, interrupted when the storage closed, or its last store was released, first, or failed with the error) and never rejects. onFailure receives each kind of failure once per process; by default it becomes a process warning. The collection does not keep your process running; if it has nothing else to do, it exits after the batch in progress.
  • runningSnapshotCollection(blobs) returns the automatic collection this process is running for that database, as a promise that settles when it ends and never rejects, or undefined when none is running. Asking for it keeps your process running until the collection ends. Close a database you opened yourself only after it settles if you want the collection to finish: closing it earlier makes the next batch fail, and the collection resolves interrupted. When the last store Atomic opened on a database closes, Atomic stops the collection itself.

Errors

Every error except GitUnavailableError extends WorkspaceError, the family the workspace binding errors belong to; all of them carry a stable code, JSON-safe fields, a hint, and retryable, which is false for every error except WorkspaceCollectionError. isRetryableWorkspaceError(error) finds a retryable one in a cause chain. Every WorkspaceError has a summary (its message without the code and hint). Check the class or code, never the message. findWorkspaceError(error, guard) finds one in a cause chain (up to 8 levels), and serializeWorkspaceError / rehydrateWorkspaceError carry them across process boundaries with their class and fields intact.