kosa8 docs

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:

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.

CallWhat 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

A complete recipe

Checkpoint before a migration, then rewind: a SQLite database, a migration with a bug, and the rewind that saves the data.