Skip to content

fix(sandbox): stop sandboxes from losing sessions when they stop - #248

Merged
quickbeard merged 3 commits into
mainfrom
fix/sandbox-guest-db
Aug 17, 2026
Merged

quickbeard merged 3 commits into
mainfrom
fix/sandbox-guest-db

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Sessions created in a sandbox disappeared. Three separate causes, all found by
taking a shell inside a running vz guest (the server's own PTY API) and reading
its database and filesystem directly.

1. The guest database was keyed to the host's git branch

A dev build takes its channel from git branch --show-current, and the server
files its database as opencode-<channel>.db for any channel that is not a
release one. The guest runs the host's bundle, so it inherited that: every time
the host repo changed branch, the guest pointed at a fresh, empty database. The
guest I looked at had three — opencode-main.db, opencode-work-folder-tabs.db
and one more — each holding the sessions from whichever branch was checked out
when they were made.

Nothing was lost, but nothing could be reached either. Guests now set
CODEV_DISABLE_CHANNEL_DB=1. Packaged builds already resolved to the same
opencode.db, so this only changes dev builds. The trade is one guest database
shared across branches: a branch that adds a migration migrates it for the rest.

2. Guest writes never reached the disk

This is the one that actually loses data, and it affects packaged builds too.

Stopping a sandbox is a power cut: native/vzhelper calls
VZVirtualMachine.stop(), which Apple documents as equivalent to pulling the
power cable, and the guest gets no chance to sync. The server's database runs
WAL with synchronous = NORMAL — durable across a crashed process, not across a
machine that goes away — so every commit still sitting in the guest's page cache
died with the VM. Stop a sandbox, add a folder to one (which restarts it), or
force quit the app, and the sessions from the preceding seconds were gone.

Measured before the fix: a file written in the guest and left unsynced is gone
after a plain Stop/Start, and so is a session created seconds earlier.

PRAGMA synchronous now reads CODEV_SQLITE_SYNCHRONOUS — whitelisted, since
the value lands in a PRAGMA, and defaulting to NORMAL so nothing changes for
anything that does not set it. Sandbox guests set FULL. The knob has to live
in core because the guest runs the host's own server bundle; there is no
sandbox-only build to patch.

Measured after: a session created two seconds before kill -9 of the whole app
is still there on the next start.

3. A force quit left the sandbox unable to start

Found while testing #2. Force quitting the app leaves the broker's unix socket
file behind, and the next start fails binding onto it:

sandbox failed to start { message: 'listen EADDRINUSE: address already in use
  /var/folders/.../codev-sbx-302a68e46f/broker.sock' }

The sandbox stays unstartable until someone deletes that file by hand. The vz
helper already unlinks its own socket before binding; the broker's is bound in
TypeScript and did not. It now clears the file, but only when nothing answers on
it — a live socket belongs to another instance of the sandbox, and failing
loudly is right there.

Verification

All against the real vz backend, driving the running dev app:

  • Before: session created → Stop/Start → gone. Unsynced guest file → gone.
  • After: session survives Stop/Start, and survives kill -9 of the app two
    seconds after being created.
  • Guest now uses a single opencode.db, which survives a host branch switch
    (checked by switching branches and re-reading the database in the guest).
  • Stale-socket regression test: bind over a leftover socket file, and refuse to
    steal one that still has a listener.

packages/codev-sandbox 147 tests pass, packages/opencode 3197 pass with the
pragma change, packages/core typechecks, oxlint clean. DIVERGENCES.md gains
a row for the two core files, CHANGELOG.md two entries, and the sandbox README
records both guest env vars and the remaining gap.

Not fixed here

The power-cut stop itself. Durable commits cover the server's database, but
anything else the guest writes to its own disk in the last seconds still goes.
A graceful shutdown — the helper asking the guest to power down, the guest
syncing first — needs a listener baked into the guest image, so it is worth
doing the next time the image is rebuilt. Noted in the README's known gaps.

Sessions written before this lands are still in their per-branch database files
under ~/.local/share/codev inside the guest; say the word and I can pull them
across.

🤖 Generated with Claude Code

quickbeard and others added 3 commits August 17, 2026 18:25
A dev build takes its channel from `git branch --show-current`, and the
server files its database as `opencode-<channel>.db` for any channel that is
not a release one. The sandbox guest runs the host's bundle, so the guest was
re-keyed to whatever branch the host repo had checked out: every branch switch
pointed it at a fresh, empty database and every session in the sandbox
vanished from the UI. Nothing was lost — one guest here had accumulated
opencode-main.db, opencode-work-folder-tabs.db and a third — but nothing could
be reached either.

Guests now set CODEV_DISABLE_CHANNEL_DB=1, which the server already honours.
Packaged builds already resolved to the same opencode.db, so this only changes
what a dev build does. The trade is one guest database shared across branches:
a branch that adds a migration migrates it for the others.

Sessions written before this change are still in their per-branch files under
~/.local/share/codev in the guest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sessions created in a sandbox disappeared when it stopped. Stopping one is a
power cut — the vz helper calls VZVirtualMachine.stop(), which Apple documents
as equivalent to pulling the power cable, and the guest gets no chance to sync
— while the server's database runs WAL with synchronous=NORMAL, which is
durable across a crashed process but not across a machine that goes away. Every
commit still in the guest's page cache died with the VM, so a Stop/Start, a
restart to apply a mount, or a force quit took the newest sessions with it.

Measured on a real vz guest: a file written and left unsynced is gone after a
plain Stop/Start, and so was a session created seconds earlier.

`PRAGMA synchronous` now reads CODEV_SQLITE_SYNCHRONOUS, whitelisted because
the value lands in a PRAGMA and defaulting to NORMAL so nothing changes for
anything that does not set it. Sandbox guests set FULL. The knob has to live in
core because the guest runs the host's own server bundle.

After: a session created two seconds before `kill -9` of the whole app is still
there on the next start.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Force quitting the app left the broker's unix socket file in place, and the
next start failed with EADDRINUSE binding onto it — the sandbox could not
start at all until someone found the file in the temp directory and deleted
it. Hit while force quitting the app to test the durability fix, so it is
exactly the state a crash leaves behind.

The vz helper already unlinks its own socket before binding; the broker's is
bound in TypeScript and did not. It now clears the file, but only when
nothing answers on it: a socket with a live listener behind it belongs to
another instance of the sandbox, and failing loudly is the right outcome
there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard force-pushed the fix/sandbox-guest-db branch from 6cf4196 to aa5791c Compare August 17, 2026 14:25
@quickbeard
quickbeard merged commit 3b62f08 into main Aug 17, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the fix/sandbox-guest-db branch August 17, 2026 14:26
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