trek-e's review found the M1 PID-liveness backport dropped two pieces of capability-lock.cts's steal-safety machinery, reopening the #500/#905/#1230 lost-update family: - Empty-body window (state.cts): acquireStateLock creates the lock with O_EXCL and writes the pid in a separate writeSync; a lock observed in that gap has an empty body, reads as not-verified-live, and was stolen at age ~0 — robbing a holder mid-creation. Add a fresh-create floor scoped to the unverifiable-body case: an empty/unparseable body that is fresh is treated as mid-creation and is NOT stolen, while a COMPLETE dead-pid body is still stolen promptly (preserves the prompt-dead-steal contract). planning-workspace writes its body atomically (flag:'wx') so it has no empty-body window. - Double-steal (both locks): the steal was a bare fs.unlinkSync with no identity re-confirm, so two waiters could both reclaim a dead holder and end up holding concurrently. Replace with an atomic renameSync (only one racer wins the inode) guarded by a (dev,ino,body) identity re-confirm immediately before the steal; body content is part of the identity to defeat inode reuse. Tests (seam-driven, no wall-clock, each proven RED-before-GREEN): - clock-seam: fresh empty-body lock is not stolen at age ~0; a racer-recreated live lock is not double-stolen (identity re-confirm). Adds a beforeSteal seam. - planning-workspace: racer-recreated live lock is not double-stolen. - Updated the two #1217 unlinkSync-failure tests to the renameSync steal path (the bounded-backoff/no-busy-spin guarantee is preserved and re-asserted). Uncontended acquire path is byte-for-byte unchanged. Claude-Session: https://claude.ai/code/session_01R88n7Q54bAaVHFkDbbH1yz
52 KiB
52 KiB