A store commit that loses the index.lock race retries once and speaks in the CLI's words (#444) - #448
Merged
Merged
Conversation
Five cells, one per test in `apps/cli/test/store-lock.test.mjs`: a lock held for the whole run, a lock let go inside the retry window, no lock at all, a server-backed store with a lock file lying in it, and a locale whose git speaks another language. The four rulings the cells are derived from: one retry after about 300 ms and no more, so the fix is not the write coordination decision 01kz57aqsxym2g2g8wasp6vv7j ruled out; one notice in the CLI's own words and exit 0 when the retry also loses; recognition by the lock's path rather than by the exit status or the translated sentence around it; and no coordination between sessions, which stays out of scope per #7. Signed-off-by: rayim <rayim@inflike.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o in the CLI's words `commitAll` staged and committed through `mustGit`, which puts git's stderr into the refusal it raises. A second session holding the store's index therefore reached the reader as git's own advice — remove the file manually — printed after this command's receipt, telling them to delete a lock a live process was holding, with a non-zero exit for a write that had succeeded. `add` and `commit` now go through `tookTheIndex`, which tries each step twice with a 300 ms `Atomics.wait` between the attempts, exactly as `human.ts` sleeps: git runs under `spawnSync`, so there is no loop turn to await on. A failure that is not the lock is raised on the first attempt, unchanged, through the sentence `mustGit` has always raised — extracted as `gitFailed` so the two paths cannot drift apart. `status --porcelain` keeps `mustGit`: it declines to refresh an index it cannot lock and still answers. Losing both attempts prints one notice — the store is being written by another session; this event is recorded and will be committed with the next write — and returns. The events are in `log.jsonl` before this is reached and the next session to commit sweeps them in, which decision 01kz57aqsxym2g2g8wasp6vv7j accepts, so nothing is lost and nothing of git's stderr is printed. The notice is said here rather than by the nine callers, for the reason the server-backed check is made here, and is silent under `--json` for the reason `pipeline.ts`'s receipt is. The lock is recognised by its path in git's stderr: the exit status is shared with every other fatal, and the sentence around the path is translated while the path is not. Nothing here removes a lock, and nothing coordinates the two sessions — #7 decided that. Five cells in `apps/cli/test/store-lock.test.mjs`, one per row of `docs/maintainers/case-tables/444-index-lock.md`. Closes #444 Signed-off-by: rayim <rayim@inflike.com> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Closes #444
Two sessions writing into one git-backed store hit git's index lock, and the
loser used to be told, in git's words, to remove a lock file a live process was
holding — printed after this command's own receipt, with a non-zero exit for a
write that had succeeded.
commitAllstaged and committed throughmustGit,and
mustGitputs git's stderr into the refusal it raises.The append was never at stake: it is in
log.jsonlbeforecommitAllisreached, and whichever session commits next sweeps it in. Several events per
commit is what decision
01kz57aqsxym2g2g8wasp6vv7j(#7) accepts. This is thewording and the exit status only.
The three decisions
addandcommitgo throughtookTheIndex, whichtries each step twice with a 300 ms
Atomics.waitbetween the attempts —the synchronous sleep
human.tsalready uses, because git runs underspawnSyncand there is no loop turn to await on. No loop, no lock of theCLI's own: a loop would be the write coordination Concurrent event writes collapse several events into one store commit #7 ruled out, arrived at
from underneath.
the store is being written by another session; this event is recorded and
will be committed with the next write — and returns, exit 0. Nothing of
git's stderr reaches the reader on this path. Any other git failure keeps
today's behaviour: the same refusal, carrying git's own stderr, from the
sentence
mustGithas always raised, extracted asgitFailedso the twopaths cannot drift apart.
index.lockin git's stderr — not by theexit status, which git shares with every other fatal, and not by the
sentence around the path, which git translates while the path itself is
never translated.
Scope is
commitAlland themustGitplumbing it needs to see stderr.status --porcelainkeepsmustGit: it declines to refresh an index it cannot lockand still answers, exit 0. The nine callers, the signature and the write path
are untouched, and nothing removes a lock or coordinates the two sessions.
The notice is said in
commitAllrather than by its callers, for the reasonthe server-backed check is made there — a caller added later would otherwise be
the one that lets git's advice through — and is silent under
--json, for thereason
pipeline.ts's receipt is: a line of prose ahead of the envelope is anagent's parse error, and the agent has its receipt either way.
The cells
docs/maintainers/case-tables/444-index-lock.md, one test each inapps/cli/test/store-lock.test.mjs. The lock is a file the suite holds, not asecond git racing this one — a real race answers differently on every machine.
.git/index.lock, standing for the whole run, whilestate addrunsindex.lockormanually; the lock still there; the event inlog.jsonl;status --porcelainshowing the log uncommitted; no new commitstatus --porcelainpending.jsonl; no notice; no git text; no commit — none of this path runsLC_ALL/LANG/LANGUAGECells 4 and 5 are the two reads
commitAllmakes from outside its argumentsthat the issue's own table left unstated: which kind of store this is, and what
language git says it in.
Gates
Run locally on the branch:
pnpm --filter superself typecheck,pnpm --filter superself build,node test/structure.mjs(base8b010621a594, ceiling 30lines, 0 dead exports), and
node --testonstore-lock.test.mjs(5/5),docs.test.mjs(12/12) andplace,context,execution,integrity,lifecycle(98/98). CI runs the whole suite.Self-adversarial pass: with the retry removed cell 2 fails and the rest pass;
with the wording reverted to
mustGitcells 1, 2 and 5 fail — the defectitself — and 3 and 4 stay green. Both reverts restored.
🤖 Generated with Claude Code