Repository navigation
Two sessions of the loop took the same three tasks, and the obvious lock does not lock #3331
Description
Two sessions of this loop ran concurrently and picked the same three tasks
from the same list of recommendations, opening PRs for all of them (#3314,
#3317). Nothing in the flow says who is working on what.
I found it the long way: a republish notice on the dashboard artifact. The
heading sets were identical — 738 = 738, zero either way — and the byte
counts matched, but the hashes did not. The only difference sat inside the
platform's injected runtime. Which meant my source file contained an injected
runtime — it was a downloaded copy of the published page — and that copy's
stamp read "pass 117 · all three", entries I had never written, sitting above
my own.
tri loop claim <name>
Git gives exactly one atomic operation over a shared remote: creating a ref
that does not exist. That is the whole mechanism.
tri loop claim pass-118 # 0 = it is yours, 1 = someone has it
tri loop claim pass-118 --who # ask without taking
tri loop claim pass-118 --release # give it back
Exit codes follow the repo's vocabulary: 0 taken · 1 refused, and the
line names the holder · 2 the attempt could not be made, which is not the
same as being refused.
The obvious version does not lock
Pushing origin/master to the claim tag succeeds for the second claimant
too. Git treats re-pushing the same value to an existing tag as a no-op:
first push exit 0
second push exit 0 <- both sessions now believe they hold it
Only a different value is rejected. Since two sessions of this loop sit on
the same origin/master almost by definition, the naive lock would have locked
nothing — and would have been worse than none, because it reports success.
So the claim is a commit no other claimant can produce: an empty tree with a
message naming this host, process and HEAD. Measured:
ALPHA claims: exit 0
BETA claims: exit 1 already held -- loop claim `probe-118` by ... pid 76989
A structural test pins that the claim is built, not borrowed; mutating it
back to rev-parse origin/master fails the suite.
And a refused push with no holder is exit 2
If the push is refused but nobody holds the claim, that is a broken push, not a
lost race. Reporting it as HELD would send the next session away from work
nobody is doing — the same shape as a gate reporting could-not-run as clean.
Mutating that arm from exit(2) to exit(1) fails the suite.
That second test needed fixing before it could kill anything: the first version
sliced from the first match holder(name) to end of file and asserted
contains("exit(2)"), which four unrelated exit-2 sites satisfy. The mutant
survived it. Pinned to the arm.
Claimed pass-118 with it while writing this.
User Scenarios
- Given two concurrent sessions of the loop run
tri loop claim pass-118, When the first session executes the claim, Then it receives exit 0 and holds the claim. - Given a session holds a claim on
pass-118, When a second session runstri loop claim pass-118, Then it receives exit 1 and the output names the current holder (host, pid). - Given a claim push is refused by the remote but no holder exists, When a session attempts the claim, Then it receives exit 2 (not exit 1).
Requirements
- FR-001:
tri loop claim <name>MUST create a unique commit (empty tree with message naming host, process, HEAD) and push it as a tag to atomically acquire the lock. - FR-002:
tri loop claim <name>MUST return exit 0 on success, exit 1 if another holder exists (naming the holder), exit 2 if the push failed for reasons other than a holder. - FR-003:
tri loop claim <name> --whoMUST query the current holder without acquiring the lock. - FR-004:
tri loop claim <name> --releaseMUST release the lock held by the current session. - FR-005: The claim commit MUST be built uniquely per claimant (not borrowed from
origin/master) so that re-pushing the same value is rejected by Git.
Success Criteria
tri loop claim pass-118from first session exits 0tri loop claim pass-118from second concurrent session exits 1 and output contains holder identitytri loop claim pass-118 --whoexits 0 and prints holder without acquiringtri loop claim pass-118 --releaseexits 0 and releases the claim- Test suite
cargo test -p trios-cli lock::claimpasses with 5 tests including "claim is built not borrowed" and "refused push with no holder is exit 2"
Boundary
- specs/tools/tri/loop.t27
- specs/port/trios/crates/trios-cli/src/lock.t27
- specs/storage/lock.t27
- specs/queen/tasks.t27
Activity
loop: a claim, because two sessions took the same three tasks
Merge origin/master into w118-loop-claim (Refs #3331)
loop: a claim, because two sessions took the same three tasks (#3332)
feat(tri): window, was the base you measured still the tip (#3380)
fix(tri): window refuses a failed fetch, a foreign record, and a rewi…
gHashTag commentedon Oct 7, 2026
Reopened 2026-10-07: the bulk close of issues without a
## Boundarysection was reverted by the owner. Nothing to do here; this comment replaces the earlier close notice.Implement Git-based claim locking mechanism
salvage(queen-3331): commit what the turn left uncommitted