Skip to content

stop losing session history to stale locks and torn writes - #123

Closed
iPeluwa wants to merge 1 commit into
iamnotstatic:mainfrom
iPeluwa:sessions-db-recovery
Closed

iPeluwa wants to merge 1 commit into
iamnotstatic:mainfrom
iPeluwa:sessions-db-recovery

Conversation

@iPeluwa

@iPeluwa iPeluwa commented Oct 16, 2026

Copy link
Copy Markdown

vibetime quietly stopped keeping my sessions: between Oct 11 and Oct 16 it twice reset ~/.vibe/sessions.json to empty, dropping 106 and then 22 sessions, with nothing on screen to say so. Three bugs in src/db.ts lined up to do it. This fixes all three.

What happened. On Oct 10 a process was killed while holding ~/.vibe/sessions.lock, leaving the dir with no pid file in it. acquireLock only treats a lock as stale when it finds a pid file older than 10 s, so this one was never cleared, and from then on every write waited out its 20 retries and took the unlocked fallback. Parallel hook processes then wrote at the same time, all through the one sessions.json.tmp, and tore it: one backup was a whole document followed by the tail of a longer one (JSON.parse fails with trailing bytes). readDb moved each torn file to .corrupt.<ts> and returned an empty list, and the next write saved that empty list over the history. I recovered the sessions from both .corrupt files by hand.

Stale locks. acquireLock now asks lockIsStale(). A lock with a pid file is stale when that file is older than LOCK_STALE_MS, as before, or when its pid no longer names a running process (process.kill(pid, 0), with EPERM counted as alive). A lock with no pid file is stale once the dir itself is older than LOCK_STALE_MS, which covers a holder killed between mkdir and writing its pid, or between removing its pid and the dir. A fresh pid-less lock is still respected, since its holder may be about to write the pid.

One tmp file per write. writeDb wrote every process's data to sessions.json.tmp. Two unlocked writers opened the same inode, interleaved their bytes, and one renamed the result into place while the other's rename failed with ENOENT and threw. The tmp name is now sessions.json.<pid>.<random>.tmp, as auth.ts already does for auth.json, and a failed write removes its tmp file. Unlocked writers can still lose each other's update, last rename wins, but neither can tear the file.

Recover instead of reset. When sessions.json does not parse, readDb now looks for the brace that closes the top-level object (skipping braces inside strings) and parses up to it. JSON cannot continue past that brace, so it ends the only prefix that can parse, and a torn write leaves a whole document there. If that prefix holds a sessions array, it is returned. Under the lock, the original is copied to .corrupt.<ts> and the recovered document is written back in one rename, so no reader finds the file missing. An unlocked reader (getSessions) only returns the recovered list and leaves the repair to the next locked write, because its rewrite could otherwise land after a locked writer and undo it. When nothing parses, behaviour is unchanged: the file is moved to .corrupt.<ts> and the list starts empty.

Checks: npm test passes, 155 of 155. test/sessions-db.test.mjs adds six tests: a pid-less lock dir older than the stale window is cleared and the write takes the lock; a fresh pid-less lock dir is left alone; a lock whose pid has exited is cleared; six processes writing unlocked behind a live lock over a 3000-session file all exit 0, leave valid JSON holding more sessions than were seeded, no .corrupt copy and no tmp files; a torn file (braces and quotes inside strings, then a trailing tail) reads back with all its sessions, and the next write repairs it and keeps the original byte for byte in one .corrupt copy; an unrecoverable file is still moved aside. On main the first, third, fourth and fifth fail, the fourth with the ENOENT rename every run.

@iPeluwa

iPeluwa commented Oct 21, 2026

Copy link
Copy Markdown
Author

Closing — keeping these fixes local rather than upstreaming. Thanks!

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