SDK reference
JavaScript SDK
@layerfs/sdk has 29 calls in four groups. Every call takes positional arguments and returns a Promise; none takes an options object except where a signature below shows one.
For coding agents: the layerfs-js-sdk skill is the guide an agent reads to work through the SDK: SKILL.md and one reference file per family, linked under each heading below. Where the skill and this page differ, this page follows the SDK source on main.
| Group | Calls |
|---|---|
project | init, list_branches |
branch | fork, list_commits, diff_commits, diff_branches, merge, publish |
fs | read, list, stat, grep, glob, diff_files, blame, write, edit, apply, mkdir, rm, cp, mv |
sandbox | create, mount, exec, status, commit, unmount, close |
How an agent uses it
Agent skillSKILL.md · How an agent works
Every tool call an agent makes is one step, and every step that changes files becomes a Commit on the agent's Branch. Two ways in: fs for cheap, targeted file work, and a Sandbox for Bash commands.
| Agent tool | LayerFS calls | Recorded as |
|---|---|---|
| Read, list, search, diff | fs.read, fs.list, fs.grep, fs.glob, fs.diff_files | Nothing: reads don't change history |
| Write, edit, many-file change | fs.write, fs.edit, fs.apply, fs.mkdir, fs.mv, fs.cp, fs.rm | One Commit per call; the whole change lands or nothing does |
| Bash | sandbox.mount → exec → commit → unmount | One Commit with everything the command changed: sources, lockfiles, node_modules, build output |
- Prefer
fsfor one small read or edit. It runs in your own process, against the Store, with no container and no mount. - Use a Sandbox for commands: installs, builds, tests, generators, and changes across many files where one command beats many calls.
- Commit after every Bash call, even a failing one, with a message naming the step and its result, such as
"step 12: npm test (exit 1)". A step that changes nothing answersUpToDateand makes no Commit.
The client
Agent skillSKILL.md · Import the SDKSKILL.md · Connect to a project
The SDK is the sdk/js folder of the LayerFS checkout. It isn't published to npm; link it into your folder once. It needs Node 20 or newer.
npm init -y >/dev/null && npm pkg set type=module
npm install --no-audit --no-fund "file:$LAYERFS_REPO/sdk/js" # the LayerFS checkoutimport { LayerFS, isRefusal } from "@layerfs/sdk";
const layerfs = new LayerFS("shop"); // a project name; no I/O happens yet
// … Store calls and Workspace calls …
await layerfs.close(); // releases this process's Storenew LayerFS(project) checks the name (1 to 63 of a-z 0-9 _ - ., starting and ending with a letter or digit) and does nothing else. The first call opens what it needs.
Two routes
- Store calls (
project,branch,fs) open the project's Store file in your process and read, save and publish there. They needstorePath. They run only on Linux, with the Store on a local ext4, xfs or btrfs filesystem shared with the daemons; anywhere else they failUnsupported, with no fallback. - Workspace calls (
sandbox.*) go to the daemon in a Sandbox container, through Docker.
Environment
Set these for every process of one run, with the same values.
| Variable | Meaning |
|---|---|
LAYERFS_HOME | Private folder (mode 0700) where LayerFS records projects and Sandboxes |
LAYERFS_STORE_PATH | The installed Store file that Store calls open in this process |
LAYERFS_DOCKER | Docker Engine socket; default /var/run/docker.sock |
LAYERFS_IMAGE | Runtime image digest, sha256:… |
LAYERFS_VOLUME | Existing Docker volume shared by the project's daemons |
LAYERFS_DAEMON | Host path to the built Linux daemon |
LAYERFS_TRACE | Optional: a file that gets one JSON line per call |
Options
new LayerFS("shop", {
storePath: "/stores/shop/store.sqlite", // else LAYERFS_STORE_PATH
sandbox: "agent-3", // an existing Sandbox this agent uses
storePolicy: { smallFileThreshold: 131072, wholeFileDepth: 16, chunkDepth: 8, metadataDepth: 8 },
trace: "/runs/7/trace.jsonl", // one JSON line per call
traceStep: "step 12",
});sandbox pins a client to one existing Sandbox, which is how a coordinator hands each agent its own: create the Sandboxes one at a time, then give each agent its id. A selected Sandbox that is closed or missing is refused, never replaced. storePolicy only matters at project.init. layerfs.close()releases the Store; the last client in a process closes it.
sdk.project
Agent skillmethods/project.md
project.init
init(sourcePath)
Returns{ kind: "Initialized", project, layer, root, store }; layer is main's first Layer
RefusesExists (already initialized: use it), InvalidInput, Unknown / Failed (stop and report)
Copies a folder into a new Store as main @ L0, once. Everything is imported, including.git and ignored files. Afterwards the files live in LayerFS; later processes with the sameLAYERFS_HOME find the project without it.
const created = await layerfs.project.init("/work/shop"); // once per project
// { kind: "Initialized", project: "shop", layer: "324afe…", root: "e34e2d…", store: "…" }await layerfs.project.init("/work/shop")await layerfs.project.init("/work/shop")const later = new LayerFS("shop")await later.project.list_branches()project.list_branches
list_branches(pageSize = 50, pagePos = 0)
Returnsa page of { name, id, base, head, forkedFrom }, main first
RefusesNoSuchProject, InvalidInput
For main, id and head are null and base is its current Layer.
const page = await layerfs.project.list_branches();
const exists = page.items.some((b) => b.name === "cart");
// items: { name, id, base, head, forkedFrom }; main has id and head nullconst page = await layerfs.project.list_branches(50, 0)if (!page.items.some((b) => b.name === "search")) { await layerfs.branch.fork("search")}sdk.branch
Agent skillmethods/branch.md
branch.fork
fork(name, fromRef = "main")
Returns{ kind: "Forked", branch, base, head }; head is null when forked from a main Layer
RefusesNameTaken, ReservedName (main), NoSuchBranch / NoSuchCommit
Creates a Branch from main, a Branch, a Commit or a Layer. Metadata only: nothing is copied.
await layerfs.branch.fork("cart");
// { kind: "Forked", branch: "cart", base: "3279da…", head: null }await layerfs.branch.fork("cart")await layerfs.fs.write("cart", "/NOTES.md", "Cart rewrite\n", "Add notes")await layerfs.branch.fork("cart.1", "cart")await layerfs.branch.fork("cart.retry", "c1a0…")await layerfs.branch.fork("cart")await layerfs.branch.fork("main")branch.list_commits
list_commits(branch, pageSize = 50, pagePos = 0)
Returnsa page of Commit records: id, branch, parent, secondParent, base, root, message, mergedFrom, time
RefusesNoSuchBranch
Newest first, along first parents. After an uncertain outcome, this is how you check whether a Commit happened.
const log = await layerfs.branch.list_commits("cart", 10, 0);
console.log(log.items.map((c) => `${c.id.slice(0, 12)} ${c.message}`).join("\n"));const p1 = await layerfs.branch.list_commits("cart", 2, 0)await layerfs.branch.list_commits("cart", 2, 2)branch.diff_commits
diff_commits(from, to, pageSize = 50, pagePos = 0, { cursor, hunks } = {})
Returns{ from, to, items, hasMore, nextCursor }; each item { path, kind, type, before, after, content, metadata, identity, hunks, hunksOmitted }
RefusesInvalidInput (not two Commits), NoSuchCommit
What changed between two Commits. kind is Added, Removed orModified. Pass the previous page's nextCursor as cursor to resume after its last path; hunks: false lists paths without line changes.
const [newest, previous] = (await layerfs.branch.list_commits("cart", 2, 0)).items;
const d = await layerfs.branch.diff_commits(previous.id, newest.id);
console.log(d.items.map((c) => `${c.kind} ${c.path}`).join("\n"));
// Page by path instead of re-walking: resume after the last path listed.
const next = await layerfs.branch.diff_branches("main", "cart", 200, 0, { cursor: d.nextCursor, hunks: false });const log = await layerfs.branch.list_commits("cart", 2, 0)const [newest, previous] = log.itemsawait layerfs.branch.diff_commits(previous.id, newest.id)branch.diff_branches
diff_branches(from, to, pageSize = 50, pagePos = 0, { cursor, hunks } = {})
Returnsthe same shape as diff_commits
RefusesInvalidInput (a Commit id), NoSuchBranch
The same, between the current heads of two Branches or main.
const d = await layerfs.branch.diff_branches("main", "cart", 200, 0, { hunks: false })await layerfs.branch.diff_branches("main", "cart", 200, 0, { hunks: false, cursor: d.nextCursor })branch.merge
merge(a, b, name, message?)
Returns{ kind: "Merged", fastForward, branch, head, root, sources, mergeBase, conflicts: [], lww }
RefusesConflict (nothing created; detail has sources, mergeBase, conflicts, lww), NoMergeBase, NameTaken, NoSuchBranch
Merges a and b into a new Branch name; a andb don't change. fastForward means one side already held the other. Paths matchinga's /.lww keep the newer version and are listed in lww. There is no overwrite option.
const m = await layerfs.branch.merge("cart", "main", "cart-release", "Merge main before publish");
console.log(m.branch, m.fastForward); // "cart-release" falseawait layerfs.branch.merge("cart", "cart.1", "cart.next", "Keep cart.1")await layerfs.branch.merge("cart", "main", "cart-release", "Merge main")await layerfs.branch.merge("cart.a", "cart.b", "cart.ab")await layerfs.fs.edit("cart.b", "/src/cart.js", …)await layerfs.branch.merge("cart.a", "cart.b", "cart.ab")branch.publish
publish(branch, commit?)
ReturnsAdded with publishedLayer, basedOn, overwroteCount, overwrote (first 50), overwroteHasMore; or UpToDate, or NoChanges
RefusesNoCommit, HeadMoved (main moved during the call), InvalidInput (a Layer id)
Makes main show the Branch head, or the given Commit, as a new Layer. It overwrites: main Layers published since the Branch's base are skipped and listed. If someone may have published since you forked, merge main in first, test the merged Branch, then publish it.
const p = await layerfs.branch.publish("cart-release");
if (p.kind === "Added" && p.overwroteCount) console.log("skipped", p.overwrote);await layerfs.branch.publish("cart-release")await layerfs.branch.fork("cart-2")await layerfs.branch.publish("cart")await layerfs.branch.merge("cart", "main", "cart-release")await layerfs.branch.publish("cart-release")await layerfs.branch.publish("cart", "c2b1…")sdk.fs
Agent skillmethods/fs-read.mdmethods/fs-write.md
Reads take a ref (main, a Branch, a Commit id or a Layer id) and see committed state only, never a mounted Workspace's unsaved changes. Paths are absolute in that view. A single-file answer starts with{ kind: "ok", snapshot, root, path }, where snapshot names what was read, such as"cart@9f3c0a1b2c4d".
Lists are pages, { items, hasMore, nextPagePos }: pageSize 1 to 1000 (default 50),pagePos from 0. A short page while hasMore is true only means it hit its byte bound. Deep offsets skip linearly, so narrow the path rather than paging far.
const all = [];
for (let pos = 0; ; ) {
const page = await layerfs.fs.glob("cart", "src/**/*.js", "/", 1000, pos);
all.push(...page.items);
if (!page.hasMore) break;
pos += page.items.length;
}Reads
fs.read
read(ref, path, startLine = 1, endLine?)
Returnstext, startLine, endLine, truncated, lineTruncated, nextLine, size
RefusesIsDirectory, Binary (NUL or invalid UTF-8), NoSuchPath
A 1-based, inclusive line range of a text file: at most 2000 lines and 48 KiB per answer. A huge single line comes back as a prefix and the next window starts at the following line.
// Read a whole file, window by window.
let line = 1, text = "";
for (;;) {
const r = await layerfs.fs.read("cart", "/src/cart.js", line);
text += r.text;
if (r.nextLine == null) break;
line = r.nextLine;
}await layerfs.fs.read("cart", "/src/cart.js")await layerfs.fs.read("cart", "/src/cart.js", 2001)await layerfs.fs.read("c2b1…", "/src/cart.js", 1, 5)await layerfs.fs.read("cart", "/public/logo.png")fs.list
list(ref, path = "/", pageSize = 50, pagePos = 0)
Returnsa page of { name, path, type, inode }; type is file, dir or symlink
RefusesNotDirectory, NoSuchPath
One folder, names in byte order.
fs.stat
stat(ref, path)
Returns{ type, inode, size }
RefusesNoSuchPath
fs.grep
grep(ref, pattern, path = "/", pageSize = 50, pagePos = 0)
Returnsa page of { path, line, text, lineTruncated }, plus skipped binary files
RefusesInvalidInput (empty pattern)
Lines containing pattern as plain text, not a regular expression, in path order then line order. It searches everything under path, node_modules included, so keep path narrow. For a regular expression, run grep -rn in a Workspace.
await layerfs.fs.grep("cart", "TAX", "/src")fs.glob
glob(ref, pattern, path = "/", pageSize = 50, pagePos = 0)
Returnsa page of absolute path strings
* and ? match within one name; **/ matches zero or more folders, so src/**/*.js includes /src/cart.js.
const page = await layerfs.fs.glob("cart", "src/**/*.js", "/", 1000, 0)if (page.hasMore) { await layerfs.fs.glob("cart", "src/**/*.js", "/", 1000, page.items.length)}fs.diff_files
diff_files(leftRef, leftPath, rightRef, rightPath)
Returnskind (Text, Equal or Binary), left, right, contentEqual, sameInode, hunks of { op, text }, hunksOmitted
Two files, at any refs and paths. op is " ", "-" or "+".
await layerfs.fs.diff_files("main", "/src/cart.js", "cart", "/src/cart.js")fs.blame
blame(ref, path, fromLine = 1)
Returnslines of { n, text, lineTruncated, commit, message }, versions, comparisonCoarse, nextLine
The Commit, and so the step, that last changed each line. comparisonCoarse means a large change was reported as one removed and added block.
await layerfs.fs.blame("cart", "/src/cart.js")Writes
Each write changes one Branch (never main) and publishes one Commit, or nothing. It merges with the Branch head it finds when it publishes: if another writer changed different files meanwhile, it lands on top; if it changed the same path, the write is refused as Conflict and nothing is committed. Every write answers one of:
| kind | Fields | Meaning |
|---|---|---|
Committed | branch, head, commit | A new Commit; commit is its record |
UpToDate | branch, head, root | Nothing changed, so no Commit |
Shared refusals: ReadOnly (main), NoSuchBranch, Conflict, HeadMoved,NoSuchPath (a missing parent: fs.mkdir first), TooLarge andInvalidInput. A file's body is streamed, so no file size is capped, but the rest of one request (paths, edit texts, message) must fit 65,519 bytes; past that it's TooLarge before anything is sent. A Workspace mounted earlier doesn't see a write until you mount again or its next Commit merges it in.
fs.write
write(branch, path, content, message?)
ReturnsCommitted or UpToDate
RefusesIsDirectory
Creates or replaces one file. content is a string, or bytes.
await layerfs.fs.write("cart", "/src/routes/cart.js", "export default {};\n", "Add route")await layerfs.fs.write("cart", "/src/routes/cart.js", "export default {};\n")fs.edit
edit(branch, path, oldText, newText, message?)
ReturnsCommitted or UpToDate
RefusesNoMatch, AmbiguousMatch, Binary
Replaces exactly one occurrence of oldText. It streams large files, so it's the cheap way to change a few lines of a big one. On NoMatch or AmbiguousMatch nothing changed: read the file as it is now and include more surrounding lines.
const r = await layerfs.fs.edit("cart", "/src/cart.js", "const TAX = 0.07;", "const TAX = 0.08;", "cart: tax 8%");
console.log(r.kind, r.head); // "Committed" "1280…"await layerfs.fs.edit("cart", "/src/cart.js", "const TAX = 0.07;", "const TAX = 0.08;", "Tax 8%")await layerfs.fs.edit("cart", "/src/cart.js", "const TAX = 0.07;", "…")await layerfs.fs.edit("cart", "/src/cart.js", "qty", "quantity")await a.fs.edit("cart", "/src/cart.js", "price * qty", "cents(price) * qty")await b.fs.edit("cart", "/src/cart.js", "price * qty", "price * quantity")await b.fs.read("cart", "/src/cart.js")await b.fs.edit("cart", "/src/cart.js", "cents(price) * qty", "cents(price) * quantity")fs.apply
apply(branch, changes, message?)
ReturnsCommitted or UpToDate
Refusesthe first failing change's refusal, with detail.index (zero-based)
An ordered list of write, edit, mkdir, rm, cp andmv changes as one Commit. Later changes see earlier ones. The best way to scaffold or make any multi-file change.
await layerfs.fs.apply("cart", [
{ op: "mkdir", path: "/src/routes" },
{ op: "write", path: "/src/routes/cart.js", content: "export default {};\n" },
{ op: "edit", path: "/src/server.js", oldText: "// routes", newText: "import cart from './routes/cart.js';" },
{ op: "mv", from: "/src/basket.js", to: "/src/legacy/basket.js" },
], "cart: add the route"); // one Commit, or nothingawait layerfs.fs.apply("cart", [ { op: "mkdir", path: "/src/routes" }, { op: "write", path: "/src/routes/cart.js", content: "export default {};\n" }, { op: "mv", from: "/src/basket.js", to: "/src/legacy/basket.js" },], "Add the route")await layerfs.fs.apply("cart", [ { op: "write", path: "/a.txt", content: "a" }, { op: "rm", path: "/missing.txt" },])fs.mkdir
mkdir(branch, path, message?)
ReturnsCommitted, or UpToDate for an existing folder
RefusesNotDirectory
Creates a folder and any missing parents.
fs.rm
rm(branch, path, message?)
ReturnsCommitted
RefusesNoSuchPath
Removes a file or a whole folder tree.
await layerfs.fs.rm("cart", "/src/legacy", "Drop legacy code")await layerfs.fs.read("c4d3…", "/src/legacy/basket.js")fs.cp
cp(branch, from, to, message?)
ReturnsCommitted
RefusesExists (to exists), InvalidInput (same path, or into its own subtree)
Copies a file or tree. Keeps modes, symlinks and hard links within the copied set; timestamps are new.
fs.mv
mv(branch, from, to, message?)
ReturnsCommitted
Refusesthe same as fs.cp
Moves or renames a file or tree, keeping its identity and timestamps.
sdk.sandbox
Agent skillmethods/sandbox.md
A Sandbox is a container running one LayerFS daemon, bound to one project and one image. Its id is the name you give it, and any later process with the same LAYERFS_HOME can use it and its mount paths. A Workspace is a complete, writable view of a Branch mounted inside it, where ordinary commands run.
- Each mount is a new Workspace with a new path, such as
/workspaces/3. Pass that path toexec,status,commitandunmount; never build one yourself. - The view is fixed at mount. It shows the Branch as it was then.
commitmerges in what moved; to see it, mount again. - Everything in it is committed: sources,
.git,node_modules, build output, caches. Keep scratch files in/tmp, outside the Workspace. - One process at a time per Sandbox. A second one gets
Busy. There's no restart recovery: if the container dies, its Workspaces are gone.
One Bash call as one Workspace:
const path = await layerfs.sandbox.mount(id, "cart"); // a NEW Workspace, e.g. "/workspaces/1"
const run = await layerfs.sandbox.exec(id, path, "timeout 110 npm test 2>&1", "step 12");
await layerfs.sandbox.commit(id, path, `step 12: npm test (exit ${run.exitCode})`);
await layerfs.sandbox.unmount(id, path); // path is now invalidsandbox.create
create(name, project, image, { volumes } = {})
Returnsthe Sandbox id, which is name
RefusesExists, InvalidInput, Busy (another create holds the project lock), Failed, Unknown
Starts a Sandbox and waits until it's ready. project must be this client's project andimage a local digest, sha256: plus 64 hex. volumes mounts existing volumes read-only outside /workspaces. Create Sandboxes one at a time. A Failed create withdetail.removed left nothing behind; Unknown blocks later creates until a person cleans up what the message names.
const id = await layerfs.sandbox.create("builder", layerfs.projectName, process.env.LAYERFS_IMAGE, {
volumes: [{ volume: "pnpm-store", target: "/pnpm" }], // optional, mounted read-only
});
// id === "builder"await layerfs.sandbox.create("builder", "shop", process.env.LAYERFS_IMAGE, { volumes: [{ volume: "pnpm-store", target: "/pnpm" }],})await layerfs.sandbox.create("builder", "shop", process.env.LAYERFS_IMAGE)sandbox.mount
mount(id, branch)
Returnsthe Workspace path, such as "/workspaces/1"
RefusesNoSuchBranch (main can't be mounted: fork it), NoSuchSandbox, Busy
Mounts a Workspace on the Branch head at this moment. It doesn't scan or copy the tree.
await layerfs.sandbox.mount("builder", "cart")await layerfs.sandbox.mount("builder", "search")await layerfs.sandbox.mount("builder", "main")sandbox.exec
exec(id, path, command, message?)
Returns{ kind: "exit:<code>", exitCode, stdout, stderr, truncated, message }
RefusesNotMounted, NoSuchSandbox, Busy, Unknown (exit code not known)
Runs bash -c command at the Workspace root (HOME=/tmp) and waits until everything holding its output has ended. A non-zero exit is not a refusal. Each stream keeps its first 1 MiB. There is no timeout, so wrap long commands: timeout 110 npm test. Use relative paths; the message labels the run and creates no history.
const run = await layerfs.sandbox.exec(id, path, "timeout 110 npm test 2>&1", "cart tests");
// { kind: "exit:0", exitCode: 0, stdout: "…# pass 3…", stderr: "", message: "cart tests", truncated: false }
// Big output: write it to /tmp, outside the Workspace, and read the end.
await layerfs.sandbox.exec(id, path, "timeout 110 npm run build >/tmp/build.log 2>&1; echo $? >/tmp/build.code");
const tail = await layerfs.sandbox.exec(id, path, "cat /tmp/build.code; tail -n 60 /tmp/build.log");await layerfs.sandbox.exec(id, path, "timeout 110 npm test 2>&1", "Run tests")await layerfs.sandbox.commit(id, path, "npm test")await layerfs.sandbox.exec(id, path, "timeout 110 npm run build >/tmp/build.log 2>&1; echo $? >/tmp/build.code")await layerfs.sandbox.exec(id, path, "cat /tmp/build.code; tail -n 60 /tmp/build.log")sandbox.status
status(id, path)
Returns{ base, baseLayer, branchHead, headMovedSinceMount, activity, overlay, branch }
RefusesNotMounted, NoSuchSandbox, Busy
overlay counts what changed since mount: { inodes, entries, pendingMerge }.
const s = await layerfs.sandbox.status(id, path);
if (s.headMovedSinceMount) console.log("the next commit merges with", s.branchHead);await layerfs.sandbox.status(id, path)sandbox.commit
commit(id, path, message?, force = false)
ReturnsCommitted with head, commit, displacedHead, merged, lww; or UpToDate with head, root
RefusesConflict, HeadMoved, NotMounted, Busy
Saves everything changed in the Workspace as one Commit; the Workspace stays mounted. If the Branch moved since the mount it merges first (merged is then { from, head }). On Conflictnothing is committed and the conflicted files in the Workspace now hold markers: fix them with commands and commit again. force overwrites the newer head instead of merging and drops the other changes; avoid it.
await layerfs.sandbox.exec(id, path, "timeout 110 npm install dayjs")await layerfs.sandbox.commit(id, path, "Add dayjs")await layerfs.sandbox.commit(id, path, "Again")await layerfs.sandbox.commit(id, path, "Tax 9%")await layerfs.sandbox.commit(id, path, "Rename qty")await layerfs.sandbox.exec(id, path, "…edit src/cart.js…")await layerfs.sandbox.commit(id, path, "Rename qty")sandbox.unmount
unmount(id, path, discard = false)
Returns{ kind: "Unmounted" }
RefusesDirty (detail.changed lists uncommitted paths), Busy, NotMounted
Deletes the Workspace's overlay; history stays. Commit first, or pass discard only for a Workspace you're abandoning.
await layerfs.sandbox.unmount(id, path)await layerfs.sandbox.unmount(id, path, true)sandbox.close
close(id)
Returns{ kind: "Closed" }
RefusesMountsActive (unmount every Workspace first), Busy, NoSuchSandbox
Stops and deletes the container and frees its name. The project and its history are unaffected. WithLAYERFS_TRACE set, save the daemon's log first: docker logs on the container id in$LAYERFS_HOME/_sandboxes/<id>/container.
await layerfs.sandbox.close("builder")await layerfs.sandbox.unmount("builder", "/workspaces/1")await layerfs.sandbox.close("builder")Errors and results
Agent skillSKILL.md · Rulesrules/conflicts.mdrules/lww.md
A refused call throws a LayerFSError with a stable code, a message for people and a structured detail. The code alone decides; isRefusal(error, code) tests for one.
import { isRefusal } from "@layerfs/sdk";
try {
await layerfs.sandbox.unmount(id, path);
} catch (e) {
if (isRefusal(e, "Dirty")) console.log("uncommitted:", e.detail.changed);
else throw e;
}Read before you retry. Every call is attempted once; LayerFS never retries or resends on its own. OnUnknown or Failed the outcome is uncertain: stop, check with branch.list_commitsand fs.read, and report.
| Code | Means | Do |
|---|---|---|
Conflict | The same path changed on both sides; nothing committed | Read both sides, fix, try again. For a Workspace, fix the markers |
HeadMoved | The head moved during the call; nothing published | Look at the Branch, then decide |
Dirty | Unmount with uncommitted changes | Commit, or discard deliberately |
Busy | Another process holds the Sandbox or lock | Run steps one after another |
NoSuchPath, NoSuchBranch, NoSuchCommit, NoSuchProject, NoSuchSandbox | The thing named doesn't exist | List it: fs.list, project.list_branches |
NoMatch, AmbiguousMatch | fs.edit found zero or several matches | Read the file again; widen oldText |
IsDirectory, NotDirectory, Binary, NotText | Wrong kind of path or content | Use a Sandbox command for binary work |
Exists, NameTaken, ReservedName | The name is taken or reserved | Use it, or pick another |
ReadOnly | A write to main | Write on a Branch, then publish |
MountsActive, NotMounted | Close with mounts, or a path not mounted | Unmount first; use the path from mount |
TooLarge | The request didn't fit; nothing sent | A smaller call, or one Sandbox step |
Unsupported | Store calls on an unsupported platform | Run on Linux with a local Store |
InvalidInput | A bad argument | Read message |
Unknown, Failed | The outcome is uncertain or failed | Stop, check, report |
Results
- Every result has a
kind:Committed,UpToDate,Forked,Merged,Added,Unmounted,Closed,exit:<code>, orokfor reads. - Messages are optional UTF-8, at most 4096 bytes; newline and tab are allowed. A message is part of the Commit's identity, and a message alone never makes a Commit.
- Ids are hex strings. A Commit or Layer id is a ref you can read, fork or diff.
- Pages are
{ items, hasMore }; ask for the next atpagePos + items.length.










