Repository navigation
test(pstack): model-check the orch store lock with TLA+ - #29
Merged
Merged
Conversation
A project skill, verify-orch-store-lock, models how orch processes take and release <store>/.orch.lock in poteto-mode's orch/store.ts, one action per filesystem call, and checks with TLC that at most one live process writes the store at a time. Without crashes the check passes. With one process dying, it finds the stale-takeover race that cursor#730 reports: two processes read the same dead holder, and the second one's unconditional unlink deletes the first one's fresh lock, so both hold it.
…am sync merge-upstream.py deletes every tracked path outside KEEP, so the next sync would have removed verify-orch-store-lock. KEEP_DIRS now covers .agents/skills/ and .claude/skills/, where project skills live for Codex and Claude Code. Upstream tracks nothing under either.
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.
Why
orchuses<store>/.orch.lockto keep two processes from writing the plan store (units.tsv,ledger.tsv,gates.md,frontier.json, the inbox) at once. Creating the lock is atomic, but taking over a stale one is not. Upstream reports a race there (cursor#730) and has a fix open (cursor#733), but its test admits it can't reproduce the cross-process case. A model checker can explore every interleaving.What changes
A project-local skill,
verify-orch-store-lock, in.agents/skills/, symlinked from.claude/skills/so that Codex and Claude Code both find it:StoreLock.tlamodelsacquireLock,takeOver,holderIsDead, and the release closure inpoteto-mode/scripts/orch/store.ts. Each action is one filesystem call orprocess.kill, andwriteFilegoes to the inode the process opened, not to whatever is at the path now.MutualExclusionis the property: at most one live process is betweenacquireLockreturning andclose().StoreLockNoCrash.cfgruns three processes that never die.StoreLock.cfgruns three processes, one of which may die at any step.check.shruns both with TLC from TLA+ v1.7.4, which it downloads once to~/.cache/tla2tools/and checks by SHA-256. It runs one worker, so a counterexample is the shortest one and the same on every run. Both configurations take about 8 seconds together.SKILL.mdrecords the covered code and the sync commit, how the model maps to the code, the expected results, how to tell a bug from a modeling error, how to update the model when the code changes, and what the model leaves out.merge-upstream.pykeeps project skills. It deletes every tracked path outsideKEEP, so the next upstream sync would have removed this skill.KEEP_DIRSnow includes.agents/skills/and.claude/skills/, and the port README lists them. Upstream tracks nothing under either.Findings
StoreLock.cfgfails in 16 states. p1 takes the lock and dies. p2 and p3 both readp1from the lock file. p2 takes over and starts work. p3 then confirms thatp1is dead, unlinks the file at the path, which is now p2's live lock, and also starts work. This is pstack orch: stale-lock takeover can be lost (unlink-then-create TOCTOU); frontier SHAs use local branches cursor/plugins#730. The fork'sstore.tsis identical to upstream's, so the fix should arrive through the sync. The skill says how to update the model when it does.openandwriteFileblocks the store. It leaves an empty lock that every later process reads as"unknown", whichholderIsDeadnever treats as dead. Everyorchrun then refuses until someone passes--force. TLC confirmed it with an extra invariant. The skill lists this under what it doesn't cover, because it's a liveness question, notMutualExclusion.Proof
StoreLockNoCrash.cfgpasses (739 states).StoreLock.cfgfails only with the trace above, the same on repeated runs.create()instore.ts,open(path, "wx")changed toopen(path, "w"). The skill'sgit diffagainst the sync commit flagged the hunk. I updated the model to match, andStoreLockNoCrash.cfgfailed in 5 states with two processes working at once. I then dropped the change in both places, and the results were back to the ones above.StoreLock.cfg(1954 states). So the failure comes from the unconditionalunlink, not from the crash model.test_the_next_sync_deletes_nothing_this_repository_tracks, which caught theKEEPgap.check.pyreports 0 problems, andbash -n check.shpasses. CI runsshellcheckon it.