feat(sessions,minimald,minvmd): session persistence contracts + provider index - #715
Closed
norrietaylor wants to merge 1 commit into
Closed
feat(sessions,minimald,minvmd): session persistence contracts + provider index#715norrietaylor wants to merge 1 commit into
norrietaylor wants to merge 1 commit into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
…der index Unit 3 of the per-VM writable ext4 volume spec (R3.1-R3.5): the operational contracts that follow from user data living permanently on the volume. - R3.1 (sessions): DiskLoader::new no longer hard-fails on a corrupt sessions/index.json — the index is derived state, so a parse failure falls back to an empty index, the existing self-heal pass rebuilds it from per-session record.json files, and a valid index is flushed back. Transient read I/O errors still fail. A torn index write now survives restarts, so this path is reachable in practice. - R3.2 (minimald): the guest boot path resets `providers/` (boot- ephemeral host keys, known_hosts, socket paths) after mounting the volume, while `sessions/` and `cache/` are untouchable by construction — no glob over the state root. - R3.3 (minimald, rescoped to the git-push path — the tar receiver in rpc.rs has no ported client, #603): a push into a session worktree with uncommitted tracked changes is rejected loudly by a new pre-receive hook ("[remote rejected] (pre-receive hook declined)", non-zero `git push` exit) instead of post-receive's silent skip; `git push -o force-checkout` overrides and discards those changes. receive-pack now advertises push options and sets receive.denyCurrentBranch=ignore — the hook pair owns worktree consistency, and git's default guard refused every re-push to the branch post-receive last checked out. Hooks live in src/git_hooks/*.sh via include_str!. - R3.4 (minvmd): new ProviderIndex — a persistent JSON map of SessionId -> { image_path, vm_id } at <state>/session_index.json, atomic rename writes, corrupt-file loads as empty (derived data). - R3.5 (minvmd): the `run` supervisor maintains the index by polling ListSessions over the existing host->guest bridge UDS (MINVMD_SESSION_POLL_SECS, default 15, 0 disables). This deliberately replaces the spec's guest->host SessionLifecycle RPC: a held-open guest->host vsock wedges against host->guest traffic (#588), and lifecycle events fire exactly while the client's own host->guest connection is open, so one-shot emits cannot be serialized safely either. Entries for other VMs — and entries for a VM that stopped — persist: the mapping is what future multi-VM routing (#311) needs. architecture.md updated accordingly (ledger row settled-by-avoidance). e2e (gated MINVMD_E2E=1, verified on macOS/HVF): a created session appears in session_index.json with this VM's image path and vm_id, a destroyed one is removed, and a session live at `minvmd stop` persists. Refs: #583 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
norrietaylor
force-pushed
the
feat/vdb-unit3-persistence
branch
from
July 10, 2026 22:21
6e782ea to
b4ceaf2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #705. Now that session worktrees and the package cache survive a VM restart on the
/dev/vdbvolume, several things that were harmless when all state died with the per-boot tmpfs become persistent traps. This PR closes them.Failure modes fixed
1. A corrupt session index bricks every session — silently, after READY.
The session store hard-failed to construct if
sessions/index.jsondidn't parse. On a tmpfs that was nearly impossible (the file was rewritten every boot); on the persistent volume a torn write survives, and an unclean stop is exactly how you get one. The daemon then reached READY but every session RPC failed against the un-constructable store — a VM that looks healthy and can touch none of its sessions, with no path back short of hand-deleting the file.Now: a parse failure is treated as recoverable (the index is derived state, not the source of truth). The store falls back to an empty index, rebuilds it from the per-session
record.jsonfiles via the self-heal pass that already existed, and writes a valid index back. A read I/O error still fails — only corruption self-heals.2. Stale provider identity persists across reboots.
providers/holds boot-ephemeral identity — the host SSH key,known_hosts, socket paths. On the tmpfs it was regenerated every boot. On the persistent volume it would carry a previous boot's host key forward, so a client that learned the new key mid-session, or any host-key rotation, would silently mismatch.Now: the guest resets
providers/after mounting the volume and before regenerating its instance dir, whilesessions/andcache/are preserved. The reset only ever touches entries insideproviders/— never a glob over the state root — so persistent user data is untouchable by construction.3. A dirty worktree silently swallows every push.
When a session's checked-out worktree had uncommitted changes, the git
post-receivehook printed a note tostderrandcontinued — the push exited 0, the refs updated, but the working tree was never checked out. The client believes it deployed new code; the guest keeps running the old code. On a tmpfs the dirty state cleared on the next boot; on the persistent worktree it sticks, so every subsequent push to that session is silently ignored until someone manually cleans the tree.Now: a
pre-receivehook rejects the push loudly —! [remote rejected] (pre-receive hook declined)and a non-zerogit pushexit — unless the client passesgit push -o force-checkout, which discards the uncommitted changes and checks out. receive-pack advertises push options and setsreceive.denyCurrentBranch=ignore(the hook pair, not git's default guard, owns worktree consistency). Recovery from a mid-checkout failure is a re-push.4. The host can't locate a session that outlives its VM.
Sessions now persist on the volume image after the VM process exits, but nothing on the host recorded which image holds which session — the mapping lived only inside the ext4 image, invisible until a VM booted it. Any future routing of
attach/activateto the owning VM (#311) had nothing to route against.Now: a host-side
ProviderIndexat<state>/session_index.jsonmaps each session id to{ image_path, vm_id }, written atomically. It's derived state — a corrupt or missing file loads empty and is rebuilt — so it never becomes its own failure mode.Transport note (why not the guest→host RPC the design sketched)
Maintaining the index was originally sketched as the guest pushing session-lifecycle events to the host. That's unimplementable safely: libkrun's vsock wedges when a guest→host connection overlaps host→guest traffic (#588 — the failure that red-lit autospawn-e2e on #672), and lifecycle events fire precisely while the client's own host→guest SSH connection is open, so even one-shot emits can't be serialized clear of it. Instead the
runsupervisor pollsListSessionsover the existing host→guest bridge (the proven direction) and reconciles the index —MINVMD_SESSION_POLL_SECS, default 15 s,0disables. Entries persist across VM stop (a stopped VM's sessions stay routable); the cost is bounded staleness, fine for a consumer that's future work. Only therunsupervisor maintains the index — bareminvmd bootleaves nothing behind to poll.architecture.mdis updated to record this.Verification
Gated e2e (
MINVMD_E2E=1, verified on macOS/HVF): a created session appears insession_index.jsonwith the right image path and vm id, a destroyed one is removed, and a session live atminvmd stoppersists in the index. Unit coverage: corrupt-index recovery (three shapes — bad bytes, wrong-shape JSON, corrupt-with-no-records), providers reset preserving sessions/cache, the git-push hook matrix (clean push, clean re-push, dirty rejection, forced override) driven against realgit, and ProviderIndex round-trip / atomic-flush / corrupt-load.Refs: #583. Full multi-VM routing on top of the index is #311.
🤖 Generated with Claude Code