Skip to content

feat(cm-playground): load a machine from a snapshot tarball - #185

Open
tuler wants to merge 4 commits into
prerelease/v2-alphafrom
claude/load-machine-snapshot-tarball-23qib2
Open

feat(cm-playground): load a machine from a snapshot tarball#185
tuler wants to merge 4 commits into
prerelease/v2-alphafrom
claude/load-machine-snapshot-tarball-23qib2

Conversation

@tuler

@tuler tuler commented Aug 29, 2026

Copy link
Copy Markdown
Member

A machine now comes from one of two places: built from the form, or loaded
whole from a stored one someone else built. A snapshot is the directory
cartesi-machine --store=<dir> writes, tarred up — it arrives here as bytes
like an image does, is unpacked into the emulator's own filesystem, and is
what load() is pointed at, resuming wherever it was stored instead of
booting from scratch.

Meeting an archive halfway is most of the work. It is gzipped as often as
not, which DecompressionStream handles with no library; it may wrap the
machine in a directory of its own, which is what tar -cf snap.tar snap
produces, so the machine is found by looking for the config.json every
stored machine has; and it is as big as the machine inside it, so the
unpacked copy is dropped the moment load() — which defaults to no sharing,
and has read the whole thing into memory — is done with it.

Snapshots share the image library's IndexedDB store under a snapshot kind,
so the same fetch-a-URL and add-a-file paths carry them, GitHub releases
through this app's proxy included. The form's source switch decides which
panels the page shows: a stored machine settled its own drives, command line
and registers when it was stored, and all this page still has a say in is
the console it is driven through and how long it runs for.

The run statistics follow: a loaded machine starts at the cycle it was
stored at, so the speed and the cycle limit both count from where this run
began rather than from zero.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01LYAATiMPkcpeS6AoWSgUvg

claude added 3 commits August 28, 2026 21:52
A machine now comes from one of two places: built from the form, or loaded
whole from a stored one someone else built. A snapshot is the directory
`cartesi-machine --store=<dir>` writes, tarred up — it arrives here as bytes
like an image does, is unpacked into the emulator's own filesystem, and is
what `load()` is pointed at, resuming wherever it was stored instead of
booting from scratch.

Meeting an archive halfway is most of the work. It is gzipped as often as
not, which `DecompressionStream` handles with no library; it may wrap the
machine in a directory of its own, which is what `tar -cf snap.tar snap`
produces, so the machine is found by looking for the `config.json` every
stored machine has; and it is as big as the machine inside it, so the
unpacked copy is dropped the moment `load()` — which defaults to no sharing,
and has read the whole thing into memory — is done with it.

Snapshots share the image library's IndexedDB store under a `snapshot` kind,
so the same fetch-a-URL and add-a-file paths carry them, GitHub releases
through this app's proxy included. The form's source switch decides which
panels the page shows: a stored machine settled its own drives, command line
and registers when it was stored, and all this page still has a say in is
the console it is driven through and how long it runs for.

The run statistics follow: a loaded machine starts at the cycle it was
stored at, so the speed and the cycle limit both count from where this run
began rather than from zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYAATiMPkcpeS6AoWSgUvg
The other direction of loading one. "Store", in the run bar, catches the
machine as it stands: it is written out, tarred, gzipped and filed in the
snapshot library, where it is a snapshot like any other — loadable again, and
downloadable as a `.tar.gz` that `tar -xzf` opens.

Compressing is not a nicety here. A machine is mostly memory nothing has
written to, so its tar is mostly zeroes: a 64 MiB machine goes from 126 MB to
309 KB, which is the difference between a snapshot worth keeping in a browser
and one that is not.

The sharing mode is not a choice. A machine created in memory — the only kind
this page has — can only be written out with `All`; the other two ask the
emulator to roll ranges back to a backing store that was never there. What it
does not mean is that the machine is then tied to the files, so the directory
is tarred and removed straight away, giving a machine's worth of memory back
while the run carries on untouched.

Storing happens beside the run rather than instead of it: the message arrives
between slices of the drive loop, which picks up where it left off. It has its
own small state in the run bar, since a store that fails is not the run
failing.

Track the app with changesets, so all of this lands in a changelog:
private packages are now versioned (never published), which gives
@cartesi/cm-playground a CHANGELOG.md and bumps the @cartesi/machine version
it pins along with that package's releases. @cartesi/docs is unaffected —
it depends on the workspace packages only in devDependencies, which
changesets does not release a package for. `baseBranch` was still "main",
which no longer exists here and left `changeset status` unable to run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYAATiMPkcpeS6AoWSgUvg
`main` is what the workflows treat as the trunk — ci, docs and release all
run on `main`, `release/*` and `prerelease/*` — so it is the branch feature
branches are cut from and merged back into, whatever branch happens to be the
default while a prerelease line is open.

Pointing it at the prerelease branch was fixing the wrong thing. `baseBranch`
reaches exactly two commands, `changeset status` and `changeset add`, which
diff against it to work out which packages a branch touched; `version`,
`publish` and `pre` never read it, in pre mode or out of it. So a baseBranch
naming a branch this clone does not have breaks those two locally and nothing
in the release, and it comes right on its own once `main` is back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYAATiMPkcpeS6AoWSgUvg
@changeset-bot

changeset-bot Bot commented Aug 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ce79b28

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@cartesi/cm-playground Minor
@cartesi/machine Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cartesi-playground Ready Ready Preview Aug 31, 2026 4:08pm

Request Review

A stored machine is the worst case for tar. A RAM image and drives that are
mostly untouched are files full of holes, and libarchive — the `tar` on macOS
— records those sparsely without being asked, in GNU's 1.0 format, where the
name in the entry header is a decoy (`GNUSparseFile.0/ram.bin`) and the real
one is in a pax record. The reader wrote the decoy and never wrote the file,
so `load()` failed later with

    unable to open backing file '…/0000000080000000-8000000.bin':
    No such file or directory when initializing RAM

GNU tar's own --sparse broke it differently: the continuation blocks holding
the sparse map were read as the next header, which failed the checksum.

All three sparse dialects are expanded now — GNU 1.0 and 0.1 through the pax
records, old-GNU `S` through the header and its continuation blocks — and pax
extended headers are parsed for `path` and the `GNU.sparse.*` keys, so long
and re-stated names arrive intact. GNU long-name entries are honoured too.

Two silences go with them, both of which turned a broken archive into a
baffling error much later rather than a clear one at the time:

- an archive that stops early is refused, instead of unpacking as far as it
  got and reporting a whole machine
- an entry the reader cannot represent is refused, instead of being skipped

Entries reaching outside the snapshot directory are refused outright, which
matters now that snapshots come from wherever a URL points.

The tests build each dialect by hand and unpack it into a Map, so they need
neither the emulator nor a tar on the host; ten of the twelve fail against
the reader they replace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYAATiMPkcpeS6AoWSgUvg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants