Skip to content

A store commit that loses the index.lock race retries once and speaks in the CLI's words (#444) - #448

Merged
tonite31 merged 2 commits into
mainfrom
fix/444-index-lock-wording
Sep 4, 2026
Merged

A store commit that loses the index.lock race retries once and speaks in the CLI's words (#444)#448
tonite31 merged 2 commits into
mainfrom
fix/444-index-lock-wording

Conversation

@tonite31

@tonite31 tonite31 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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. commitAll staged and committed through mustGit,
and mustGit puts git's stderr into the refusal it raises.

The append was never at stake: it is in log.jsonl before commitAll is
reached, and whichever session commits next sweeps it in. Several events per
commit is what decision 01kz57aqsxym2g2g8wasp6vv7j (#7) accepts. This is the
wording and the exit status only.

The three decisions

  1. One short retry. add and commit go through tookTheIndex, which
    tries each step twice with a 300 ms Atomics.wait between the attempts —
    the synchronous sleep human.ts already uses, because git runs under
    spawnSync and there is no loop turn to await on. No loop, no lock of the
    CLI'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.
  2. The CLI's words, never git's. 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, 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 mustGit has always raised, extracted as gitFailed so the two
    paths cannot drift apart.
  3. Recognised by the lock's path, index.lock in git's stderr — not by the
    exit 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 commitAll and the mustGit plumbing it needs to see stderr. status --porcelain keeps mustGit: it declines to refresh an index it cannot lock
and 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 commitAll rather than by its callers, for the reason
the 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 the
reason pipeline.ts's receipt is: a line of prose ahead of the envelope is an
agent's parse error, and the agent has its receipt either way.

The cells

docs/maintainers/case-tables/444-index-lock.md, one test each in
apps/cli/test/store-lock.test.mjs. The lock is a file the suite holds, not a
second git racing this one — a real race answers differently on every machine.

# Seeded state Expected
1 a held .git/index.lock, standing for the whole run, while state add runs exit 0; the receipt; exactly one CLI-worded notice; no line holding index.lock or manually; the lock still there; the event in log.jsonl; status --porcelain showing the log uncommitted; no new commit
2 the same lock, removed 150 ms after the command starts the retry lands the event's own commit, and no notice
3 no lock the printed bytes are the receipt, the review line and the entity id, exactly as before; one new commit; a clean status --porcelain
4 a server-backed store with a lock file lying in it exit 0; the append queued in pending.jsonl; no notice; no git text; no commit — none of this path runs
5 a held lock under a French LC_ALL/LANG/LANGUAGE the same as cell 1 — recognition is on the path, which git does not translate

Cells 4 and 5 are the two reads commitAll makes from outside its arguments
that 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 (base 8b010621a594, ceiling 30
lines, 0 dead exports), and node --test on store-lock.test.mjs (5/5),
docs.test.mjs (12/12) and place, 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 mustGit cells 1, 2 and 5 fail — the defect
itself — and 3 and 4 stay green. Both reverts restored.

🤖 Generated with Claude Code

inflike-rayim and others added 2 commits September 4, 2026 13:54
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>
@tonite31
tonite31 merged commit 0822b3e into main Sep 4, 2026
3 checks passed
@tonite31
tonite31 deleted the fix/444-index-lock-wording branch September 4, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A store commit that loses the index.lock race prints git's 'remove the file manually' advice

2 participants