Conversation
withSessionLock creates the lock with open(path, "wx") and records the pid as a separate step, so a holder that died in between left an empty file. Stale-lock recovery skipped it because there was no pid to check, and every later run on that origin waited out the 30s deadline and failed with no way back. Release a lock carrying no usable pid once it is older than a short grace period, so one that is only momentarily empty still belongs to its live holder.
This branch has not been deployed
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.
withSessionLockcreates the lock withopen(path, "wx")and records the pid as a separate step:A holder that dies between those two statements leaves the lock file in place and empty.
removeStaleLockthen has no pid to check against a running process, andNumber("")is0, so it takes thepid <= 0branch and reports the lock as live:From then on every
tempo walletrun touching that origin spins for the full 30 seconds and fails withTimed out waiting for session lock: …, with no recovery short of deleting the file by hand. A truncated or otherwise unparseable first line behaves the same way.Fix
Route the no-usable-pid case to
removeAbandonedLock, which releases the lock once its mtime is older than a 5s grace period. The grace matters: the same empty file is legitimate for the moment betweenopenandwriteFile, so deleting it on sight would let two holders run at once. A lock that does carry a pid is untouched and still checked withprocess.kill(pid, 0).This trades the permanent wedge for the usual lock-timeout assumption — that a holder cannot stall for more than the grace period between two adjacent statements. Closing the window entirely would mean linking the pid file into place instead of writing it after creation, which is a larger change and would still need this recovery path for a truncated lock.
Tests
Adds
test/session-lock.test.ts: the empty lock and the unparseable-pid lock are recovered; a just-created empty lock is left alone and its holder still runs; a dead pid is still reclaimed; a live pid is still respected; the lock is removed after the callback. Ages are set withutimesso the cases are deterministic and the file runs in ~1.4s.The two recovery tests fail on
main, each hitting the vitest timeout while the loop runs down its 30s deadline; the other four pass either way and are there to pin the exclusion behaviour.Validation
vitest run test/session-lock.test.ts— 6 passed.pnpm typecheck,pnpm test:types,pnpm check:lintandpnpm changelog:validateare clean.I could not use the full suite as a signal: 10 of its files fail on my machine on an unmodified
mainfor environmental reasons —spawn sqlite3 ENOENT(no sqlite3 available here, and I cannot install it) plus 10s timeouts — and the same 10 files fail with and without this change.pnpm check:formatalso fails onmain, on.agents/friction-log/20260831180632-immutable-releases-were/friction.md, which this branch does not touch.