Repository navigation
Publish rename-lane's Rule 3 handoff stamp through a same-directory temporary file and one atomic rename, so a writer killed mid-stamp leaves the handoff whole - #212
Merged
Conversation
…t: the fix done: tests/test_rename_publication_atomic_158.py, the proving test written first. Its fault cases inject a crash inside the stamp's write and a kill, a failure and a SIGTERM between the write and the replace, for a moved and an in-place handoff. A normal-path case pins the splice bytes, the mode and the committed blob. Not yet run: the scoped run was queued behind another lane's suite on the shared lock and was stopped for the swap. left: run the test on main to show it fails; make the Rule 3 stamp in lanes-edit.sh rename-lane (~:14383) publish through a same-directory mktemp + cp -p + cat + mv -f, as publish-handoff does (~:16285), plus cleanup() removal of that temporary; rerun; open the PR; watch CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…emporary file and one atomic rename The stamp was written with `cat <stamped copy> > <handoff>`, and the redirect truncates the handoff before the first stamped byte lands: a writer killed inside that `cat`, or a machine that lost power there, left a handoff cut short that every later read took for the real one, and `rename_undo` -- a trap -- never runs on SIGKILL. The stamped bytes now go to an exclusive `mktemp` file beside the handoff in the physical directory it resolves to, take its mode with `cp -p`, and replace it with `mv -f`: `publish-handoff`'s own mechanics, inlined because that verb takes the lock this write holds. `cleanup` removes the temporary on every handled exit. The splice proof, the symlink refusals, the mutex and the rollback bookkeeping are unchanged. This makes the stamp atomic, not the rename: the four moves' crash recovery stays the separate question #158 names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 22 hours and 59 minutes by commenting |
Reviewer's GuideThe PR hardens Rule 3 handoff publication by staging the stamped content beside the resolved handoff, preserving its mode, and atomically replacing it with one rename; cleanup and focused end-to-end fault tests cover normal, crash, signal, and replacement-failure paths without claiming broader rename crash recovery. Sequence diagram for atomic Rule 3 handoff publicationsequenceDiagram
participant RenameLane
participant Handoff
participant TempFile
participant Reader
RenameLane->>Handoff: cd -P resolve parent directory
RenameLane->>TempFile: mktemp <handoff>.rename.XXXXXX
RenameLane->>TempFile: cp -p handoff
RenameLane->>TempFile: cat stamped output > temp file
RenameLane->>Handoff: mv -f temp file to handoff
Reader->>Handoff: read handoff
Handoff-->>Reader: whole original or whole stamped file
alt write or replace fails
RenameLane->>TempFile: rm -f temporary file
RenameLane->>RenameLane: die with exit 5 and rollback
end
Flow diagram for crash-safe handoff stamp replacementflowchart TD
A[Prove Rule 3 splice] --> B[Resolve physical handoff directory with cd -P]
B --> C[Create exclusive adjacent temporary file]
C --> D[Copy handoff mode with cp -p]
D --> E[Write stamped bytes to temporary file]
E --> F{mv -f succeeds}
F -->|yes| G[Temporary file becomes handoff in one rename]
F -->|no| H[Remove temporary file]
H --> I[Rollback and exit 5]
E -. writer killed or power loss .-> J[Original handoff remains whole]
G --> K[Cleanup removes temporary path reference]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
…the environment never names a path cleanup removes `cleanup` runs on every exit and removes `pbh_tmp`, `rl_htmp` and `TMPD` by name, and none of them was set at top level: with `rl_htmp=<file>` exported, a read-only `canon-lane` deleted that file (the review of #212), and `TMPD=<dir>` made it `rm -rf` the directory. `pbh_tmp` and `TMPD` had the hole on main. They are now set empty before the trap is installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ur-file rename is still not (#224) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l traps remove against an inherited value The audit #213's review asked for: `cleanup`, `rename_undo` and `release_lock` remove paths through `pbh_tmp`, `rl_htmp`, `TMPD`, `SE_CACHE_FILE`, `RL_SNAP`, `LANES_IDX_DIR`, `LANES_WT_IDX_DIR`, the `RL_*` undo paths and `LOCK`. All but `SE_CACHE_FILE` were set at top level before the trap once `pbh_tmp`, `rl_htmp` and `TMPD` were. `SE_CACHE_FILE` was first assigned at the state-event cache, so a signal before that line removed whatever it inherited. It now starts empty beside the other three, and the environment test exports a victim path under every one of these names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This was referenced Oct 17, 2026
brettheap
pushed a commit
that referenced
this pull request
Oct 17, 2026
Brings in #195 (aa14de3), #196 (983d8c7) and #212 (2732d59) on top of aaa7bc8, so the T018 selection runs on the tree the PR will be judged on. The only file both sides change is lanes-edit.sh: main's #212 edits cleanup's temporaries and rename-lane's handoff stamp, the branch adds project-legs and lane-reconcile's leg and paired-root reads. git merged it with no conflict, the hunks are disjoint, and bash -n passes on the result. Lane: openRepoTools-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap
pushed a commit
that referenced
this pull request
Oct 18, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Oct 18, 2026
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 #158
What was wrong
rename-lanepublishes the Rule 3RENAMED from …stamp into the handoff after proving the splice, and it did that with a direct redirect:cat -- "$rl_htmpd/out" > "$rl_hfile_new"(lanes-edit.sh:14383at f87fd5a). The redirect truncates the handoff before the first stamped byte is written. If the writer is killed inside thatcat, or the machine loses power there, the handoff is left cut short, and every later read takes the short file for the real one. The existing EXIT/INT/TERM rollback (rename_undo) never reaches that state, because a trap does not run on SIGKILL or a power loss.The fix
The stamped bytes now go to an exclusive
mktempfile beside the handoff, in the physical directory it resolves to (cd -P).cp -pgives that file the handoff's mode, andmv -freplaces the handoff in one rename. A reader sees the handoff whole before the stamp or whole after it. These arepublish-handoff's mechanics (lanes-edit.sh:16285–16288at f87fd5a), inlined because that verb takes the writer lock this write already holds.cleanupremoves the temporary file on every handled exit. A file a crash leaves behind is named<handoff>.rename.XXXXXX, which no reader of*.mdtakes for a handoff.Unchanged: the splice proof, the symlink refusals (a leaf link is still refused before the first byte, so replacing the leaf rather than writing through it is the same file), the mutex, and the rollback bookkeeping (
RL_H_OLD/RL_H_NEWand the snapshot).This makes the stamp atomic, not the rename. As #158 asks, the broader crash recovery of the four moves is not claimed; it is now tracked as #224. A crash between two of them still leaves them apart.
rename_undo's own restores (log, handoff, alias table, register) are still in-place redirects. The register is written that way on purpose, because of the symlink hazard in the file header.Two side effects, both shared with
publish-handoff:…-lane-<old>.md), the handoff's directory must now be writable as well as the file. The moved shape already needed that for itsmv. In a read-only directory the rename now refuses with exit 5 ("no temporary file could be made beside…") and rolls back whole, where f87fd5a stamped the file in place and exited 0. A test pins this.Review fix round
The independent review's verdict was FIX:
rl_htmpwas never set at top level, socleanup(which runs on every exit) deleted whatever path that name inherited from the environment. Withrl_htmp=<file>exported, a read-onlycanon-lanedeleted the file.pbh_tmphad the same hole on main, and so didTMPD, whichcleanupremoves withrm -rf, so an exportedTMPD=<dir>deleted the whole directory (managed-projection reads this checkout's row as the writers do, lane-trees tells an unlistable trees directory from no inventory, and the manual's seven-column row is qualified (#157 items 4a, 4c, 4d, 4e) #213's review foundTMPDtoo).cleanupand the two helpers it calls,rename_undoandrelease_lock:pbh_tmp,rl_htmp,TMPD: not set at top level; now set empty.SE_CACHE_FILE: first assigned only at the state-event cache (line 3090), so a signal between the trap and that line removed the inherited path. Now set empty.RL_SNAPand the undo'sRL_LOG_NEW,RL_H_NEW,RL_ALIAS, armed byRL_ACTIVE: already set at lines 1148–1151.LANES_IDX_DIR,LANES_WT_IDX_DIR: already set at lines 1083 and 1089.LOCK, armed byLOCK_HELD: already set at lines 1077 and 1091.index_nudgeremoves nothing, and these are the only threetraps in the file.trap cleanup EXIT. The environment test exports a victim path under each of the nine names, with the arming flag beside it where there is one.skills/handoff/SKILL.mdstill called rename's stamp "an explicit exception to this atomic publication contract". Lines 218–219 now say the stamp is atomically replaced and the four-file rename still is not crash-atomic (rename-lane: the four-file rename is still not crash-atomic, deferred from #158 #224). Checked withgit merge-tree --write-treein a private clone at 66a866e: clean against Handoff skill: resolve lanes-edit.sh on PATH first, correct the comments that still promised write (c) without a workstation, and refuse the object-log and register-line writes for a lane that has never had a session recorded #207, against Handoff skill: commit the handoff in the checkout it was published in, by its own path, and refuse foreign staged entries and in-progress operations (#197) #199, against both together, and against every other open PR (26). The same stale claim indocs/README-lanes.md:~4202–4206is inside lane openRepoTools-3's fenced region and is recorded in rename-lane: the four-file rename is still not crash-atomic, deferred from #158 #224 instead.Functions and line ranges edited (at this head)
lanes-edit.sh:TMPD=""; pbh_tmp=""; rl_htmp=""; SE_CACHE_FILE="", placed directly abovecleanup()and beforetrap cleanup EXIT.cleanup(), line 1217: one line added that removesrl_htmp, rename-lane's temporary stamp file, beforerename_undo.rename-lanedispatcher arm, step (d), the handoff stamp's publication: lines 14395–14419 replace the one-line redirect at old line 14383. That covers the comment, thecd -Presolution,mktemp,cp -p+cat+mv -f, and the two new refusals, both exit 5 like the write refusal they sit beside.skills/handoff/SKILL.md: lines 218–219 only.Nothing else in the file is touched. That keeps clear of
commit_push(#194), :12621/:16616 (#157),retire_rows/archive_rows(#138), and lines ~13000–13350 and ~16940–16960.Tests
New file
tests/test_rename_publication_atomic_158.py. It runs a realrename-laneintest_lane_start_claude_current's sandbox: a bare origin, a workspace clone, a row and a log from a reallane-start --no-launch, and a handoff seeded at the path the row names. There is no network and no real lane data. The first three tests below run for both handoff shapes, one moved and then stamped and one stamped in place: 20 cases in all. Faults are injected bycatandmvstubs first onPATH. Each stub passes every other call through to the real utility.test_rename_stamp_is_the_same_splice_and_keeps_the_handoffs_mode: the normal path. The stamp is at line 5, and taking it back out gives the original bytes. The mode stays 0640, no stray file is left, and the committed blob equals the file.test_rename_writer_killed_around_the_stamp_leaves_the_handoff_whole[crash-mid-write|kill-before-replace]: SIGKILL of the writer inside the stamp's write, and between the write and the replace. The handoff holds the original bytes, whole.test_rename_replace_refused_or_interrupted_rolls_back_and_leaves_no_temporary[fail-replace|term-before-replace]: a failing replace (exit 5) and a SIGTERM between write and replace. The existing rollback restores every file, HEAD is unchanged, and no temporary file is left. On the signal path that last part iscleanup's doing.test_rename_in_place_handoff_in_a_read_only_directory_refuses_and_rolls_back: exit 5, "no temporary file could be made beside", a full rollback and no temporary file left. Skipped when run as root.test_cleanup_removes_no_path_a_variable_inherits_from_the_environment[…]: 9 cases, one for each ofrl_htmp,pbh_tmp,TMPD,SE_CACHE_FILE,RL_SNAP,LANES_IDX_DIR,LANES_WT_IDX_DIR,rename_undo(RL_ACTIVE=1withRL_LOG_NEW/RL_H_NEW/RL_ALIAS) andrelease_lock(LOCK_HELD=1withLOCK). Each exports a victim path under that name, and the victim must survive a read-onlycanon-lane. The cases for names that were already set are guards, so they pass on main too.SE_CACHE_FILE's gap is a signal before its assignment, which a read-only command cannot reach, so its case also passes on main.Every run used the shared lock,
flock -w 1800 ${TMPDIR:-/tmp}/openrepotools-pytest.lock …, withpython3 -m pytest tests/test_rename_publication_atomic_158.py -q -p no:cacheprovider. The f87fd5a and fcfd1b4 runs used agit archivecopy of that commit holding this test file.First round (lock taken 18:59:38Z):
8 failed, 2 passed. Only the normal path passes. Bothcrash-mid-writecases leave the handoff as its first two lines (assert b'# Session h...ne: repoZ-1\n' == b'# Session h...-\nHistory.\n'), and the other three fault cases fail on "the fault never fired".10 passed.This fix round (lock taken 20:05:55Z):
-k inherits_from_the_environment:3 failed, 6 passed.rl_htmp,pbh_tmpandTMPDdeleted the victim.-k "read_only_directory or inherits_from_the_environment":3 failed, 7 passed. The read-only case exits 0 there, andpbh_tmpandTMPDdelete the victim.rl_htmpdoes not exist on main.20 passed in 37.52s.bash -n lanes-edit.shpasses. The hygiene text rules for non-ASCII after$nameand for acaseinside$( )find nothing in the changed lines. No new exit code is used. CI's Linuxtestsjob is the suite of record.🤖 Generated with Claude Code