Skip to content

fix(workspace-registry): serialize mutations and stop emptying a corrupt registry - #1

Draft
andreiverdes wants to merge 7 commits into
mainfrom
pr1-registry-durability
Draft

andreiverdes wants to merge 7 commits into
mainfrom
pr1-registry-durability

Conversation

@andreiverdes

Copy link
Copy Markdown
Member

Layer 1 of 3. Base: main.

Two failure modes in ~/.beadbox/registry.json:

  • Interleaved writes. The sidecar handles rpc calls concurrently, so two overlapping mutations could interleave and lose one. Because the write used writeFile in place, an ill-timed pair could leave a half-written file that no longer parses.
  • A read failure emptied the file. An unreadable registry was treated as "no workspaces", and the next mutation persisted that emptiness — turning a transient read error into permanent loss.

Mutations now serialize through a process-wide queue and land via tmp-file + atomic rename. Unparseable content is quarantined to registry.json.corrupt-<ts> exactly once; read errors propagate instead of presenting as an empty registry.

Known limit: the queue is per-process, so two Beadbox instances can still lose an update. Measured, not assumed — two concurrent processes produced 2 lost updates across 36 sampled reads with 0 parse failures. The file always stays valid.

Recovery drills (6/6): ENOENT yields an empty registry; unreadable propagates EACCES; truncated JSON quarantines exactly once; valid-JSON-with-malformed-entries survives; a symlink at the tmp path is refused with EEXIST and never followed; no .tmp survives a failure.

…upt registry

Two overlapping rpc calls that each did readRegistry() + writeRegistry()
interleaved and corrupted a user's ~/.beadbox/registry.json: the loser's
whole-file writeFile truncated in place, leaving unparseable bytes, and
readRegistry's catch-all then turned that into an empty workspace list
which the next write made permanent.

- writeRegistry writes a randomUUID-suffixed tmp file with
  { flag: "wx", mode: 0o600 } and renames it into place, unlinking the
  tmp on failure, so a reader never observes a partial registry.
- New mutateRegistry() serializes read-modify-write through one
  process-wide queue and skips the write when the mutation changed
  nothing; every mutation now goes through it.
- readRegistry only treats ENOENT as "missing"; any other read failure
  propagates instead of degrading to an empty registry.
- A registry that exists but does not parse is quarantined to
  registry.json.corrupt-<ts> and logged. The quarantine gate covers
  JSON.parse only: a structurally odd entry inside otherwise valid JSON
  is skipped by deduplicateEntries rather than costing the user the
  whole list.
Beadbox and others added 6 commits September 12, 2026 10:11
AI-assisted PRs are welcome. Our default is to fix, split, or partially
land a valuable PR ourselves rather than request changes, with
Co-authored-by attribution and release-notes credit. States the hygiene
that makes that possible and that external PRs land as a single squashed
commit.
Beadbox is built and maintained with initech, a runtime for agents that
collaborate from one terminal. A two-line note under the title so readers
find it.
…t (beadbox-0qx)

The eight `dtolnay/rust-toolchain@4360b52 # stable` pins (quality-gates,
release, build-arm-dmg) went red on zizmor's impostor-commit audit
(exit 14). Not an impostor: 4360b52's parent is on upstream master. The
upstream `stable` branch is regenerated on every Rust release and its
history rewritten, so the pinned commit is no longer reachable from any
ref — which is exactly the property the audit checks. Re-pinning to the
current `stable` head would go red again at the next release.

Pin d1031067 instead: on master (never rewritten) and the exact base the
current `stable` branch was generated from, with `toolchain: stable`
passed as an input — the action's README recommends this form for
full-length SHA pins.

zizmor 1.29.0 with the gate's exact invocation: 8 impostor-commit
findings before, 0 after. actionlint clean.
…nge (beadbox-0qx)

The dependency-audit gate (`bun audit --audit-level=high`) started
failing on advisories published after the last push:

  browserslist <=4.28.6  GHSA-c83g-rgw3-j3cx, GHSA-73wf-gq98-2v4g
  fast-uri     <3.1.6    GHSA-jqff-g426-hqxp, GHSA-f65p-4m7j-42xc
  nanoid       <3.3.12   GHSA-xwg4-73v4-xw9w

All three are dev-scope transitives (babel via the router plugin, ajv via
the mutation tester, postcss via vite); none ships in the app.

Lockfile-only bump inside the dependents' declared ranges: browserslist
4.28.9 (plus its five data packages), fast-uri 3.1.7, nanoid 3.3.19.
package.json untouched. `bun install --frozen-lockfile` clean from an
empty node_modules; the audit gate goes from 5 high to 0.
…adbox-0qx)

`mutation-rust` runs 96 cargo-mutants mutants at ~70 s each against a
30-minute job budget, and each mutant executes 0 s of tests — so the job
burned ~30 minutes of runner time per push for no information. Disabled
by an always-false `if:` on the job (actionlint rejects a literal
`if: false`), with the job and its configuration left intact so the
restore is a one-line change once the mutant runs actually execute
tests (tracked as beadbox-l5i.8). Header comment updated to match.
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.

2 participants