Repository navigation
Findings deferred from #97: fetch-first reads, the live-swap takeover, a hand-written RESUMED, and the five of the 8c5958f round #157
Description
This issue holds the findings deferred from Copilot's review of #97: three single findings (sections 1-3) and the five of the last round, on 8c5958f (section 4). Each was declined on that PR, with its reasoning in its thread, and they are filed here so that none is lost.
1. The lifecycle reads fetch the register first
From Copilot's review at ec9847e (the thread).
The finding
lane-reconcile calls log_sync before it reads. So do lane-state, lane-trees and managed-projection. With the default environment, a direct lanes-edit.sh lane-reconcile <lane> therefore runs ls-remote and then fetch against the register checkout's origin/<branch>:
- Offline, each step can wait up to
LANES_GIT_TIMEOUT(60 s by default) before it falls back to the ref as it stands. - Online, the fetch moves that checkout's remote-tracking ref.
Copilot asks for these lifecycle reads to stay local.
Why #97 keeps the fetch
- What these arms read is published state.
canon_laneresolves the name through the published register and Amendment 16's alias table. A rename that landed elsewhere is seen only after a fetch, and Crash-consistent lane worktree recovery: a lane now says WHERE its session stopped — RUNNING → SWAPPING → SWAPPED with a generation/operation fence, a machine-readable worktree inventory taken at every handoff, and a resume reconciliation that reports and resets nothing #97 moves the control root to the new name.lane-reconcilereads the lane's published binding (Amendment 18(b)). On a stale ref it would pronounce a crash from a binding that has since moved to another host.lane-reconcilealso reads the managed-owner marker. Design.md's risk list already names a no-fetch caller missing a marker published since its last fetch.
- Every read in
lanes-edit.shdoes this.log_syncis the first act ofbinding,who,lanes,lane-dirand the rest. It always returns 0, and when it fails it falls back to reading offline; it never refuses. - It touches no tree. The fetch goes into the register checkout. The reconciliation's boundary ("it resets nothing") is about the lane's worktrees, and it still holds.
- The launch path already skips it.
lane-startfetches in its step 3, then callslane-reconcilewithLANES_NO_FETCH=1. A direct caller has the same opt-out, and Crash-consistent lane worktree recovery: a lane now says WHERE its session stopped — RUNNING → SWAPPING → SWAPPED with a generation/operation fence, a machine-readable worktree inventory taken at every handoff, and a resume reconciliation that reports and resets nothing #97's manual now says so.
What would decide it
The question is which default an operator's recovery read during a network outage should get:
- Keep the fetch: the read pays the bounded wait.
- Read locally: the read may see a stale binding, alias table and marker.
The options:
- Keep the fetch, and say so in the manual. Crash-consistent lane worktree recovery: a lane now says WHERE its session stopped — RUNNING → SWAPPING → SWAPPED with a generation/operation fence, a machine-readable worktree inventory taken at every handoff, and a resume reconciliation that reports and resets nothing #97 does this.
- Add a
--no-fetchflag to the lifecycle reads. - Give the read-only arms a shorter timeout.
Making local the default would bring back the stale-binding verdict that Amendment 18(b) exists to prevent.
2. A second swap supersedes a live one
From Copilot's round 2 at d2d2eec (the thread), on lane-handoff:434.
The finding
lifecycle_begin (lane-handoff:426-435) treats every lane still recorded SWAPPING as abandoned and supersedes it with a new generation and operation. It never asks whether the recorded operation's handoff is still running. A second lane-handoff can therefore take over a swap that is live, and the first process's finalizer and inventory writes then race the new operation. The spec scenario "Competing swap begins" said such a swap must not mint another owner.
Why #97 keeps the takeover
-
It is the settled answer for an interrupted swap. design.md's Open Questions settle "complete or supersede?" as supersede, and the new generation is what refuses the old finalizer if it ever wakes. A lane left
SWAPPINGby a session that ran out of tokens is the case this change exists for, and refusing the next handoff there would strand the lane. -
A live swap cannot be told from an interrupted one today. That takes the handoff process's liveness, and the snapshot does not record it. The snapshot carries schema, lane, state, generation, operation, owner (a transcript uuid), agent, profile, workstation and kind. design.md's risk list rules out a PID alone: "Use the existing binding/session provenance contract, not PID alone."
-
The fence bounds the harm. Every lifecycle write of the superseded operation is refused with 7 and changes nothing:
- its
SWAPPEDfinalizer (set-lane-state --expect-generation/--expect-operation), covered by repoRC-2 (a stale finalizer) and repoRC-3 (a finalizer after the generation moved); - its inventory writes (
set-lane-tree --generation/--operation), covered by repoRC-5.
What is left is the two processes' non-lifecycle handoff writes running at once. The lifecycle may not stop those, because
R-A11-11says a swap is never left unwritten. - its
What #97 changed (eada5ff)
- The spec scenario now says what the code guarantees: the recorded operation is named and superseded, and its later finalizer and inventory writes are refused. The requirement names this deferral.
- design.md's settled answer says what it cannot yet tell.
- tasks.md files the finding in section 7. Task 6.2 (races) stays open for it.
What would decide it
A liveness record for the handoff process that a second handoff can read and trust. That means a holder written at SWAPPING and cleared at SWAPPED, checked through the binding/session provenance contract rather than a PID. With it, lifecycle_begin could refuse a live competitor (or report it, without minting an owner) and keep superseding an interrupted one.
3. A hand-written RESUMED moves the lifecycle
From Copilot's review at eada5ff (the thread), on lanes-edit.sh:4949 (and :11314).
The finding
write_event's lifecycle follow-up runs for any STARTED or RESUMED line, including one written by hand: LANES_LANE=<lane> lanes-edit.sh log RESUMED lane:<lane> …. Such a line takes an existing snapshot to RUNNING, but nobody proved the canonical checkout, transcript, agent or exclusive binding that the spec's "Running ownership is confirmed" requirement assigns to the confirming act. ENDED and RETIRED to CLOSED follow the same path.
Why #97 keeps it
- It is design decision 9, on purpose. "
RUNNINGfollows THAT line, inwrite_event— the register's single writer, which already validates the session field and Amendment 17(b)'s sub-fields there for the same reason: from any caller, over one implementation." The line is the prooflane-startleaves. A second writer path forRUNNINGwould be a second implementation of it. - A hand-written
RESUMEDis a recorded act, not a silent one. It goes into the append-only log under a transcript uuid the writer validates, is committed and pushed before the snapshot moves, and is fenced by the pre-image (decision 16). The suite relies on it: repoRC-3, "a recovery resumes the lane while a swap is still recorded in flight", is a hand-writtenRESUMEDtaking the laneRUNNINGunder a new generation, which is what refuses the old swap's finalizer. - The seam already bounds who can do it. Since the 2026-10-04 ruling, every lane-kind line refuses a managed-owned lane or one whose ownership is unknown, so this applies to legacy lanes only.
What would decide it
Whether RUNNING needs a proof the log line cannot carry. One option is a token that only lane-start's confirmation holds, checked by write_event before lane_state_follow. The other is to restrict the follow-up to a flag that only the confirming path passes, and to say what a person recording a recovery by hand should run instead. Either one changes decision 9 and the repoRC-3 case.
4. The round on 8c5958f: five findings, filed rather than taken
Copilot reviewed 8c5958f after the round that #97 took as seam completeness. The coordinator's rule for that point: a finding is filed here unless a legacy writer can still stamp a managed lane. None of these can, under the published register of record.
4a. A local-only marker reaches lane-start's transcript move (lane-start:1435, thread)
- The finding.
lane-start'sseam_refuseaskslanes-edit.sh managed-projection, which reads the published register only. Since07fb3ad,lanes-edit.sh's writers also read this checkout's row, and refuse a local row carrying vocabulary the published row lacks. The scripts' gate does not. So with an unpushed marker in this checkout only,lane-startpasses its gate and can runfollow_transcript(lines 2795/2845) before a laterlanes-edit.shwriter refuses. - Why filed (the coordinator's ruling, 2026-10-04). A published marker is still refused before any write, which is the ruling's guarantee. The gap needs a managed-owner marker committed in this checkout and not pushed, and branch 001's own contract (T024) is "enrollment publishes before launch", so the managed path promises never to leave that state. The managed ledger publishes its markers, and no legacy writer can write the vocabulary. The register, the log and the control root are still protected, because every
lanes-edit.shwriter refuses. - The fix, when taken. The
managed-projectionarm returns 1 when this checkout's row carries vocabulary and the published read says 8. That makeslane-start,lane-handoffandlane-end's gates match the writers, and needs a case with an unpushed local marker.
4b. A sweep's capture commit precedes its seam refusal (lanes-edit.sh:10386, :10760, :11080, thread)
- The finding.
migrate-state-cells,retire-rowsandarchive-rowscallhandle_preexistingbefore their scans. In no-sweep mode it commits a peer's uncommittedLANES.mdedit as its ownLANES(pre-existing@…)commit, so a refusal in the scan, the seam's included, leaves HEAD moved. - Why filed. The capture is a peer's edit, never this act's, and these acts take it before the scan on purpose: they rewrite rows by line number (Copilot round 2 on Amendment 13: the register row says where a lane IS in one line, and its diary moves to the lane's own log —
set-row-state,NOTED/RULED,history,migrate-state-cells;append-row-statusretired #82). main's own refusals in the same scans (retire-rows' "named twice" and its separator-count refusals, the migration's skips, the archive's separator refusal) all sit after it already. So the fix belongs to all of them together: a preflight that applies every refusal, the seam's included, before the capture, while keeping the skip rules.capture_register_edit's own comment states the principle ("a refusal that had already committed a peer's register edit would be a refusal that wrote").
4c. A non-seven-column row without vocabulary reads as legacy (lanes-edit.sh:11240, thread)
- The finding. The ported reader answers 8 when
row_split_state_cellfails and the row carries no managed-owner vocabulary. - Why filed. That is the reader ported verbatim from
3c26041(branch 001's T019), and the empty diff is its provenance. A row with no vocabulary cannot be a marker, so legacy is the right ownership answer. The row writers then refuse a malformed row on their own (row_split_state_cell's exit 2). One correction is owed: Crash-consistent lane worktree recovery: a lane now says WHERE its session stopped — RUNNING → SWAPPING → SWAPPED with a generation/operation fence, a machine-readable worktree inventory taken at every handoff, and a resume reconciliation that reports and resets nothing #97's manual table and PR body list "a row that is not seven columns" under exit 1 unqualified. They should say "a row carrying the vocabulary that is not seven columns".
4d. The same, for this checkout's row (lanes-edit.sh:11368, thread)
- The finding.
managed_seam_refuse's local check asks only for the vocabulary, so a malformed local row without it passes as legacy. - Why filed. It is the same answer as 4c: a row with no vocabulary cannot be a marker. The writer that then rewrites the row refuses its shape itself.
4e. An unreadable trees/ directory reads as no inventory (lanes-edit.sh:11859, thread)
- The finding. If the tree-sidecar directory exists but cannot be traversed, the glob yields nothing,
lane_trees_listreturns 8, andlane-reconcilecan report aSWAPPEDlaneresumablewithout having read a tree. - Why filed. It is The lifecycle's remaining fail-open reads: an unreadable register becomes 'no holder', an unreadable sidecar becomes no tree, and two git reads become fabricated values #113's family, a read that turns a failure into an answer. 8.12 took the unreadable sidecar FILE; the unreadable directory is the same rule one level up. The fix is to tell absent from unreadable (
[ -d ] && ! [ -x ], or a failed listing) and make the verdictindeterminate.
Lane: openRepoTools-3
🤖 Generated with Claude Code
Activity
Say that the reconciliation reads the published register first
Defer the live-swap takeover to #157 and list the migration's seam
[-]The lifecycle reads fetch the register first: decide whether lane-reconcile, lane-state and lane-trees should read locally by default[/-][+]Two findings deferred from #97: the lifecycle reads fetch first, and a second swap supersedes a live one[/+][-]Two findings deferred from #97: the lifecycle reads fetch first, and a second swap supersedes a live one[/-][+]Three findings deferred from #97: the lifecycle reads fetch first, a second swap supersedes a live one, and a hand-written RESUMED moves the lifecycle[/+][-]Three findings deferred from #97: the lifecycle reads fetch first, a second swap supersedes a live one, and a hand-written RESUMED moves the lifecycle[/-][+]Findings deferred from #97: fetch-first reads, the live-swap takeover, a hand-written RESUMED, and the five of the 8c5958f round[/+]Crash-consistent lane worktree recovery: a lane now says WHERE its se…
brettheap commentedon Oct 11, 2026
CLAIMED — lane openRepoTools-4, session e545e636-93fd-4b80-81b1-48177346b887@Eagle, 2026-10-11T16:37:46Z, for #157
Logged in
lanes/log/openRepoTools-4.mdatb9aa8cac47cd0c3b45d6e79d14a3c3c8e968a925.The three reads (lane-collision-protocol Rule 1):
Tell an unlistable trees directory from no inventory in lane-trees (#157
Read this checkout's row in managed-projection too, and qualify the m…
Name the register file read and both directions in the local-copy ref…
The lane manual names the new refusals: the sweep's and the archive's…
managed-projection reads this checkout's row as the writers do, lane-…
Register sweeps and reads refuse what they could not read: retire-row…
Lifecycle reads that failed are no longer answers, set-lane-tree's fe…
brettheap commentedon Oct 21, 2026
Closing on Brett Heap's ruling of 2026-10-21: "yes to all six rulings as recommended" (lane 4's R3, R4 and R5: keep).
LANES_NO_FETCH=1opts out.87a5e9d); 4d needed no code change and is pinned by a test;5e88a2c);00d52ae).Nothing is left open here.
Lane: openRepoTools-4
brettheap commentedon Oct 21, 2026
RELEASED — lane openRepoTools-4, session e545e636-93fd-4b80-81b1-48177346b887@Eagle, 2026-10-21T10:44:32Z, for #157 — items 4a to 4e fixed by #213, #230 and #252 and items 1 to 3 kept on Brett Heap's rulings R3 to R5 of 2026-10-21; #157 is closed