Guides / JavaScript & TypeScript / Checkpoints and rewind
Checkpoints and rewind
A checkpoint saves the whole running machine: memory, processes and disks. A rewind puts the sandbox back to it under the same name, still running. Take one before anything you could not otherwise undo.
Checkpoint, change, rewind
import { Kosa8Client } from '@kosa8/sdk';
const kosa8 = new Kosa8Client();
const db = kosa8.sandbox('db');
await kosa8.createSandbox({ name: 'db', egress: 'deny' });
await db.pull('alpine:latest');
// A process with state in memory: a counter that only lives in the shell's variable.
const cid = await db.startContainer('alpine:latest', ['sh', '-c',
'echo v1 > /schema; i=0; while :; do i=$((i+1)); echo $i > /tmp/tick; sleep 1; done']);
const sh = async (c) => (await db.exec(cid, ['sh', '-c', c])).stdout.trim();
await new Promise((r) => setTimeout(r, 2000));
const a = await db.checkpoint({ label: 'before migration' });
console.log('took', a.checkpoint.id, `"${a.checkpoint.label}"`, 'tick', await sh('cat /tmp/tick'));
await sh('echo v2 > /schema');
const b = await db.checkpoint({ label: 'after the schema change' });
console.log('took', b.checkpoint.id, `"${b.checkpoint.label}"`);
await sh('rm /schema');
console.log('stack:', (await db.checkpoints()).map((c) => `${c.id} (${c.label})`));
// Back to the first one by label. The later checkpoint describes a future that did not happen.
const rw = await db.rewind({ to: 'before migration' });
console.log(`rewound to ${rw.checkpoint.id} in ${rw.restore_ms}ms; dropped`, rw.dropped);
console.log('schema:', await sh('cat /schema'), '| tick:', await sh('cat /tmp/tick'));
await new Promise((r) => setTimeout(r, 2000));
console.log('2s later, tick:', await sh('cat /tmp/tick'), '(the same loop, still running)');
console.log('stack:', (await db.checkpoints()).map((c) => c.id));
$ node ckpt.mjs
took db-ckpt-1 "before migration" tick 3
took db-ckpt-2 "after the schema change"
stack: [
'db-ckpt-1 (before migration)',
'db-ckpt-2 (after the schema change)'
]
rewound to db-ckpt-1 in 509ms; dropped [ 'db-ckpt-2' ]
schema: v1 | tick: 3
2s later, tick: 5 (the same loop, still running)
stack: [ 'db-ckpt-1' ]
What the output shows:
- Processes come back. The counter loop was not restarted. After the rewind it reads 3, its value at the checkpoint, and two seconds later it reads 5. The same container id still works.
- Files come back.
/schemawas changed to v2 and then deleted; after the rewind it is v1 again. - Later checkpoints are dropped.
db-ckpt-2described a state the machine no longer reached, so the rewind deleted it and its snapshot, and says so indropped. totakes a checkpoint id or a label, compared without case. Without it,rewindgoes to the most recent checkpoint.
The CLI sees the same checkpoints
By default the SDK keeps its list in ~/.kosa8/checkpoints.json, the file the CLI,
the MCP server and the Mac app use. So a checkpoint your program took can be rewound by hand, and
the other way round:
$ kosa8 checkpoint ls db
CHECKPOINT WHEN TAKEN WHY
*db-ckpt-1 just now you before migration
* is where `kosa8 rewind db` goes
How checkpoints work
The daemon has no checkpoint endpoint. A checkpoint is an ordinary
snapshot with the id <sandbox>-ckpt-<n>,
plus a record of its label and order kept on the client side, exactly as the CLI does it.
| Call | What it does |
|---|---|
checkpoint(id, { label? }) | Takes the snapshot, then records it. Returns { checkpoint, evicted, snapshot }. |
listCheckpoints(id) | The sandbox's checkpoints, oldest first: { id, sandbox, label, created, auto? }. |
rewind(id, { to? }) | Deletes the sandbox, restores the checkpoint's snapshot under the same name, then drops later checkpoints. Returns { sandbox, checkpoint, dropped, restore_ms }. |
deleteSandbox(id) | Also deletes the sandbox's checkpoints and their snapshots, unless keepCheckpoints: true. |
Where the list is kept
import { Kosa8Client, MemoryCheckpointStore, FileCheckpointStore } from '@kosa8/sdk';
// Default: the CLI's own file, so `kosa8 checkpoint ls` and `kosa8 rewind` see these.
const shared = new Kosa8Client();
console.log('default store:', shared.checkpoints.store.path);
// Private to this process: nothing is written to disk, and the CLI never sees them.
const isolated = new Kosa8Client({ checkpointStore: new MemoryCheckpointStore() });
await isolated.createSandbox({ name: 'scratch', egress: 'deny', memMb: 512 });
const c = await isolated.checkpoint('scratch', { label: 'private' });
console.log('memory store has:', (await isolated.listCheckpoints('scratch')).map((x) => x.id));
console.log('file store has: ', await shared.listCheckpoints('scratch'));
// A file of your own, e.g. one per CI job.
const perJob = new Kosa8Client({ checkpointStore: new FileCheckpointStore('/tmp/k8d/job-42-checkpoints.json') });
console.log('per-job store:', perJob.checkpoints.store.path);
// The snapshot behind a checkpoint is an ordinary snapshot; remove both with the sandbox.
await isolated.deleteSandbox('scratch').then((r) => console.log(r));
$ node ckptstore.mjs
default store: /tmp/k8d/.kosa8/checkpoints.json
memory store has: [ 'scratch-ckpt-1' ]
file store has: []
per-job store: /tmp/k8d/job-42-checkpoints.json
{ ok: true, checkpoints_dropped: 1 }
Any object with load() and update(fn) can be a store
(CheckpointStore), for example one backed by your database.
Things worth knowing
- Ten per sandbox. The eleventh checkpoint evicts the oldest
(
MAX_CHECKPOINTS_PER_SANDBOX), deleting its snapshot only after the new one exists.evictedlists what went. (Not exercised for this page: eleven snapshots would not fit the test machine's disk.) - A checkpoint costs memory-sized disk. It saves the guest's memory as well as cloning its disks. In our runs a 1 GiB sandbox checkpointed in about 0.65 s (656 ms in the quickstart, 647 ms in the migration recipe).
- A rewind destroys the current state and cannot itself be undone. Take a checkpoint first if you might want the current state back.
- If the restore step fails, the sandbox has already been removed. The error is a
Kosa8Errorwhose message ends with therestoreSnapshot(…)call that brings it back, because the checkpoint's snapshot is still there. - On a Mac, the screen must be unlocked for a rewind. Taking a checkpoint works while it is locked; restoring it does not. From kosa8 0.9.36 the error says "macOS blocks restoring a VM while the screen is locked; unlock and retry". See Troubleshooting.
- Same instant, two writers. The SDK serialises its own updates and those of other Node processes with a lock file. The Go tools lock the same file differently, so an SDK update and a CLI update in the same instant could lose one record. The snapshots are unaffected.
- Under signed approvals, a rewind makes two guarded calls (delete, then restore) with
the same
approvalid, and one approval covers one call. See Approvals.
Checkpoint before a migration, then rewind: a SQLite database, a migration with a bug, and the rewind that saves the data.