fix(reconcile): give the prune lock a Windows backend - #52
Merged
Conversation
ProtocolWarden
force-pushed
the
claude/windows-reconcile-lock
branch
from
August 3, 2026 19:38
5138b0e to
6976b73
Compare
`reconcile_lock` raised RuntimeError on any host without `fcntl`, so `prune --apply` was unrunnable on Windows. The reconciliation workflow dead-ended there, which is why consumer logs kept getting hand-pruned instead. `msvcrt.locking(LK_NBLCK)` is the direct analogue: non-blocking, exclusive, released on process death, and it conflicts with a second handle opened by the same process, so the existing nested-acquire test still holds. One difference shapes the file layout. `flock` is advisory and whole-file, but a `msvcrt` byte range is mandatory: a reader touching a locked byte gets PermissionError. The lock therefore claims a sentinel byte at offset 1024 while the pid stays at offset 0, where a contending run can still read it to name the holder. The pid is written as a fixed-width field rather than truncate+write, because truncation would cross the locked range. Both invariants have a test. Contention is matched on errno rather than exception type (`flock` reports EWOULDBLOCK, `msvcrt` reports EACCES and EDEADLOCK). An OSError outside that set is re-raised unchanged -- a bad fd must never be reported as "someone else holds it", which the previous BlockingIOError-only catch got right by accident and a broad `except OSError` would have got wrong. Not merely an unblock: this fixes 9 pre-existing Windows test failures, all the same RuntimeError -- four lock tests and five prune tests. Suite goes 25 failures -> 16, and the remaining 16 are a strict subset of the previous set (diffed by name, not counted). 457 -> 473 passed = 9 fixed + 7 new. 7 new tests: a backend exists on this platform; the pid stays readable while the lock is held (fails if the sentinel byte ever moves onto the pid field); the layout invariant; a stale longer pid cannot leave a tail; cross-process exclusion; reacquire after the holder is killed; and an unexpected OSError not being laundered into PruneLockHeld. The cross-process test has the child print its own `os.getpid()` rather than trusting `Popen.pid` -- a venv's python.exe can be a launcher shim, so the process that takes the lock is not always the one Popen returns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ProtocolWarden
force-pushed
the
claude/windows-reconcile-lock
branch
from
August 3, 2026 19:40
6976b73 to
bc2062a
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.
reconcile_lockraisedRuntimeErroron any host withoutfcntl, socl reconcile prune --applywas unrunnable on Windows:The whole reconciliation workflow dead-ended there — which is likely why consumer
.console/log.mdfiles kept getting hand-pruned instead of reconciled.The fix
msvcrt.locking(LK_NBLCK)is the direct analogue offlock(LOCK_EX|LOCK_NB): non-blocking, exclusive, released on process death, and it conflicts with a second handle opened by the same process — so the existing nested-acquire test still holds unchanged.One difference shapes the file layout.
flockis advisory and whole-file, so any reader can read the pid. Amsvcrtbyte range is mandatory: a reader touching a locked byte getsPermissionError. So the lock claims a sentinel byte at offset 1024 while the pid field stays at offset 0, where a contending run can still read it to name the holder. The pid is written as a fixed-width field rather thantruncate+write, because truncating would cross the locked range. Both invariants have their own test.Contention is now matched on errno rather than exception type (
flockreportsEWOULDBLOCK,msvcrtreportsEACCES/EDEADLOCK). AnOSErroroutside that set is re-raised unchanged — a bad fd must never be reported as "someone else holds it", which a broadexcept OSErrorwould have got wrong.It fixes 9 pre-existing failures, not just the CLI
Every one was the same
RuntimeError:Suite: 25 failures → 16, 457 → 473 passed (9 fixed + 7 new). The remaining 16 are a strict subset of the previous set — diffed by name, not counted, so no failure was swapped for another.
New tests (7)
A backend exists on this platform; the pid stays readable while the lock is held (fails if the sentinel byte ever moves onto the pid field); the layout invariant; a stale longer pid cannot leave a tail; cross-process exclusion; reacquire after the holder is killed; and an unexpected
OSErrornot being laundered intoPruneLockHeld.The cross-process test has the child print its own
os.getpid()rather than trustingPopen.pid— a venv'spython.execan be a launcher shim, so the process that takes the lock isn't always the onePopenreturns. That cost a false failure before it was understood.Verification
ruffclean on both changed files (repo-wide ruff is red onmainat 204 findings — unrelated, unchanged)🤖 Generated with Claude Code