Skip to content

fix(widget-dev): remove leftover and orphaned snapshots at boot before any folder is watched - #668

Merged
mrgoonie merged 3 commits into
mainfrom
fix/657-prune-widget-dev-snapshots
Oct 8, 2026
Merged

mrgoonie merged 3 commits into
mainfrom
fix/657-prune-widget-dev-snapshots

Conversation

@mrgoonie

@mrgoonie mrgoonie commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #657

What leaked

Widget dev sessions removed superseded snapshots only after an install. A session that never installs again kept its leftovers, including snapshots whose removal was held (EBUSY on Windows) or skipped by close(). Past WIDGET_DEV_SNAPSHOTS_MAX (64), the oldest digests dropped off a session's list, and a session the store forgot to make room took its list with it. Nothing ever removed those folders.

What changes

  • Prune on resume. Before any folder is watched again, resume prunes every stored session, live or stopped, with the same prune an install runs. It keeps what the session runs, what it waits on and the snapshot a rollback returns to.
  • Orphaned snapshots ledger. The package cache's local/ folder also holds the snapshots an ordinary install of a local package takes. No session may remove those, and nothing on disk tells them apart from a session's. So a digest that leaves every session's list is written to widget-dev/orphaned-snapshots.json before the session store drops it. That covers the oldest past the list bound, and the snapshots, running and pending digests of a session forgotten by makeRoom. The ledger is a separate file, so the session store's format does not change, and it holds at most 1024 digests.
  • Bounded sweep at boot. After the prunes, resume removes at most WIDGET_DEV_SWEEP_MAX (64) orphaned snapshots per boot. It skips any that a session lists again, runs or waits on, and any that a generation row still names. A failed removal stays in the ledger for the next boot.
  • Removals use the existing async rm with SNAPSHOT_REMOVAL. No rmSync.

Ordering invariant

The boot's tidying removes a snapshot only while no session watches a folder. Only a watched session's engine builds. A build can reuse a folder already at its content-addressed name before the session remembers it, so a removal running beside that build could pull the folder from under an install. The ordering prevents that:

  • resume watches no folder until the tidying ends.
  • start awaits the tidying before it watches a folder, and nothing is awaited between that point and watch.
  • Each removal starts only while live.size === 0. This ends the tidying early if a session is watched anyway, for example when resume is called again while sessions are live.
  • After close(), no removal starts. close() returns early from resume, and the tidying's own chain is in chains, so close() waits for the removal in flight within its bound.

Tests (apps/runtime/test/widget-dev-sessions.spec.ts, "what widget dev sessions leave in the package cache")

  • leftovers of a stopped session (removals held as EBUSY) are removed on resume; the running and rollback snapshots stay;
  • the oldest snapshot pushed off a full list is ledgered, then swept at the next boot;
  • the snapshots of a session forgotten by makeRoom are swept at the next boot;
  • the running and pending snapshots that no list names are kept, while an unused orphan is swept;
  • a start made while a sweep removal is held waits for it, and its build of the same bytes keeps its snapshot;
  • nothing is removed at a boot of a node that is already closing.

The first five fail with main's widget-dev-sessions.ts. The fifth also fails when only the await tidying in start is removed.

Docs

EN and VI docs/widget-development(.vi).md: cleanup at the next start, and the cache note on what is collected. docs/manifest.json was refreshed.

Not covered

Folders leaked before this change were never written to the ledger. They cannot be told apart from an ordinary install's snapshots, so they stay.

…e any folder is watched

Superseded snapshots were pruned only after an install, so a session that never installed again kept its leftovers, and digests that fell off a session's list or left with a forgotten session stayed in the package cache for good. Resume now prunes every stored session, and sweeps a bounded number of orphaned snapshots recorded in a ledger beside the session store, while no session watches a folder: a start waits for it.
…n guards, and tighten the orphan list

A snapshot no session lists but a generation still names (installed, or the one a rollback returns to) is kept by the boot sweep, and a resume made while a session is watched removes nothing; both guards now have tests that fail without them. The orphan list accepts only snapshot digests and says why its bound keeps any leak bounded.
@mrgoonie

mrgoonie commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Review attestation: ready to merge at dc5c3070d736d8b1f064a68ecd69b0a6b3e0134a, reviewed by agent:code-reviewer.

A push to this PR makes this attestation stale; the new head needs its own review.

@mrgoonie
mrgoonie enabled auto-merge (squash) October 8, 2026 06:37
@mrgoonie
mrgoonie merged commit ec96bac into main Oct 8, 2026
22 checks passed
@mrgoonie
mrgoonie deleted the fix/657-prune-widget-dev-snapshots branch October 8, 2026 06:48
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.

widget dev: superseded snapshot folders leak when no install follows

1 participant