Skip to content

feat(sandbox): offer the newer guest image, and keep the guest's home - #272

Closed
quickbeard wants to merge 2 commits into
mainfrom
feat/sandbox-image-updates
Closed

quickbeard wants to merge 2 commits into
mainfrom
feat/sandbox-image-updates

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Reset already re-provisioned from the current manifest, so an image upgrade existed — but nothing ever said one was available, so guests stayed on the image they were created with until somebody happened to reset, which throws away everything inside the sandbox on the way. This is the README's one listed known gap.

Detection

Every provisioned sandbox already recorded the asset it was built from. The new SandboxProvider.imageStatus compares that marker's digest with the published one — digests, not versions, since the same version string has been republished with different bytes. A sandbox built from a custom image file reports custom and is never offered anything: the manifest is not its source.

The controller sweeps every sandbox, skipping any with a job in flight, and the desktop app asks shortly after launch, every six hours, and whenever Settings → Servers opens, sharing one memoized manifest fetch. A check that cannot reach the storage host leaves the previous verdict alone rather than reporting "up to date" about a question nobody answered.

Keeping the home

Taking the update keeps the guest's home — its sessions, its config, the files the user made — which is what separates it from Reset, still a wipe. What that takes is entirely per-backend, which is why it is a method rather than a flag on reset:

  • bubblewrap — the home is a host directory, so it is simply not deleted. Only the rootfs is replaced, wholesale rather than unpacked over the old one.
  • WSL — a distro cannot be imported over itself, so /home/codev is tarred out to the host before the old one is unregistered (the export runs while nothing has been destroyed yet, so failing it costs nothing) and tarred back in after. Both directions go over stdio, which keeps the archive binary-safe and Windows paths off the command line.
  • vz — the home lives in the disk being replaced, so the outgoing disk is set aside and attached read-only to the new guest, whose init copies the home across before starting the server; start drops it once the server answers, since a healthy guest is proof the copy finished. codev-vmm learns to attach extra disks for it.

Both backends that stage the home outside the sandbox refuse to overwrite one an interrupted update left behind: that archive, or that disk, holds the home the sandbox is still trying to keep, and the current guest's — freshly imported by the run that failed — cannot be worth more.

The macOS caveat, made explicit

The code that reads the outgoing disk lives in the image being updated to, so images now declare home-migration and the host knows before downloading anything whether an update can keep the home — the confirmation says so, rather than finding out afterwards. It takes effect once an image built from this pipeline is published; an update to today's 1.18.4-8 is a fresh start and says as much. The WSL and bubblewrap halves work against the images already published.

Elsewhere

The offer appears where sandboxes are used: a line on the Work sidebar, and a tag plus an Update sandbox item on the Settings → Servers row, both behind a confirmation that states what survives. Nothing downloads on its own — it is over a gigabyte and the sandbox restarts to take it. A successful provision now also prunes staged images no sandbox's marker names any more; nothing else ever removed them, so updating would otherwise have left its predecessor on disk forever.

Verification

  • bun test in packages/codev-sandbox: 189 pass / 2 skip (+31 tests). Typechecks clean in codev-sandbox, app and desktop.
  • Real VM on macOS through script/e2e-vz.ts --migrate (new flag): create 7.3s, start 2.4s, health/path/broker OK, migration disk attached and dropped after a healthy start. The extra mount: FAILED line in that run comes from an old cached image predating mounts.json and reproduces without the flag.
  • In the running dev app: the sidebar notice, the dialog copy and the settings tag all render, and all of it disappears when there is no update.

🤖 Generated with Claude Code

Reset already re-provisioned from the current manifest, so an image upgrade
existed — but nothing ever said one was available, so guests stayed on the
image they were created with until somebody happened to reset, which throws
away everything inside the sandbox on the way.

Detection is a marker comparison the providers already had the pieces for:
each provisioned sandbox records the asset it was built from, and the new
SandboxProvider.imageStatus compares its digest with the published one.
Digests, not versions — the same version string has been republished with
different bytes. A sandbox built from a custom image file reports "custom" and
is never offered anything: the manifest is not its source. The controller
sweeps every sandbox (skipping any with a job in flight) and the desktop app
asks shortly after launch, every six hours, and whenever Settings → Servers
opens, sharing one memoized manifest fetch. A check that cannot reach the
storage host leaves the previous verdict alone rather than reporting "up to
date" about a question nobody answered.

Taking the update keeps the guest's home — its sessions, its config, the files
the user made — which is what separates it from Reset, still a wipe. What that
takes is entirely per-backend, which is why it is a method rather than a flag
on reset:

- bubblewrap: the home is a host directory, so it is simply not deleted. Only
  the rootfs is replaced, wholesale rather than unpacked over the old one.
- WSL: a distro cannot be imported over itself, so /home/codev is tarred out to
  the host before the old one is unregistered — the export runs while nothing
  has been destroyed yet, so failing it costs nothing — and tarred back in
  after. Both directions go over stdio, which keeps the archive binary-safe and
  Windows paths off the command line.
- vz: the home lives in the disk being replaced, so the outgoing disk is set
  aside and attached read-only to the new guest, whose init copies the home
  across before starting the server; start drops it once the server answers,
  since a healthy guest is proof the copy finished. codev-vmm learns to attach
  extra disks for it. The code that reads that disk lives in the image being
  updated *to*, so images now declare `home-migration` and the host knows
  before downloading anything whether this update can keep the home — and says
  so in the confirmation rather than finding out afterwards. It takes effect
  once an image built from this pipeline is published.

Both backends that stage the home outside the sandbox refuse to overwrite one
an interrupted update left behind: that archive, or that disk, holds the home
the sandbox is still trying to keep, and the current guest's — freshly
imported by the run that failed — cannot be worth more.

The offer appears where sandboxes are used: a line on the Work sidebar, and a
tag plus an Update item on the Settings → Servers row, both behind a
confirmation that states what survives. Nothing downloads on its own; it is
over a gigabyte and the sandbox restarts to take it.

A successful provision now also prunes staged images no sandbox's marker names
any more. Nothing else ever removed them, so updating would otherwise have left
its predecessor on disk forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard force-pushed the feat/sandbox-image-updates branch from ed0d065 to 8a53340 Compare August 19, 2026 06:46
Pruning ran after a successful provision, which covered the update that
supersedes an image but not the delete that abandons one: destroy removed the
sandbox's own directory and left the image it was built from — over a gigabyte —
in the cache, where nothing would ever have collected it. Deleting is the one
moment a user expects the disk back.

Each backend prunes after its destroy, with the same rule the provision path
uses: an image any remaining sandbox's marker still names is kept, so deleting
one of two sandboxes on the same image costs the other nothing. The deleted
sandbox's marker went with its directory, so no keep list is needed.

On WSL it sits in destroy rather than in the discard() that update() also calls.
Pruning between an update's discard and its re-provision would drop an image an
interrupted earlier attempt had already staged and charge the user another
download for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard quickbeard closed this Aug 22, 2026
@quickbeard
quickbeard deleted the feat/sandbox-image-updates branch August 22, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant