Skip to content

Findings deferred from #97: fetch-first reads, the live-swap takeover, a hand-written RESUMED, and the five of the 8c5958f round #157

Closed

Description

@brettheap

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

  1. What these arms read is published state.
  2. Every read in lanes-edit.sh does this. log_sync is the first act of binding, who, lanes, lane-dir and the rest. It always returns 0, and when it fails it falls back to reading offline; it never refuses.
  3. 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.
  4. The launch path already skips it. lane-start fetches in its step 3, then calls lane-reconcile with LANES_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:

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

  1. 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 SWAPPING by a session that ran out of tokens is the case this change exists for, and refusing the next handoff there would strand the lane.

  2. 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."

  3. The fence bounds the harm. Every lifecycle write of the superseded operation is refused with 7 and changes nothing:

    • its SWAPPED finalizer (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-11 says a swap is never left unwritten.

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

  1. It is design decision 9, on purpose. "RUNNING follows THAT line, in write_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 proof lane-start leaves. A second writer path for RUNNING would be a second implementation of it.
  2. A hand-written RESUMED is 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-written RESUMED taking the lane RUNNING under a new generation, which is what refuses the old swap's finalizer.
  3. 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's seam_refuse asks lanes-edit.sh managed-projection, which reads the published register only. Since 07fb3ad, 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-start passes its gate and can run follow_transcript (lines 2795/2845) before a later lanes-edit.sh writer 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.sh writer refuses.
  • The fix, when taken. The managed-projection arm returns 1 when this checkout's row carries vocabulary and the published read says 8. That makes lane-start, lane-handoff and lane-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-rows and archive-rows call handle_preexisting before their scans. In no-sweep mode it commits a peer's uncommitted LANES.md edit as its own LANES(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-status retired #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)

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)

Lane: openRepoTools-3

🤖 Generated with Claude Code

Activity

  1. changed the title [-]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[/+] on Oct 4, 2026
  2. changed the title [-]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[/+] on Oct 4, 2026
  3. changed the title [-]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[/+] on Oct 4, 2026
  4. brettheap commented on Oct 11, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoTools-4, session e545e636-93fd-4b80-81b1-48177346b887@Eagle, 2026-10-11T16:37:46Z, for #157

    Logged in lanes/log/openRepoTools-4.md at b9aa8cac47cd0c3b45d6e79d14a3c3c8e968a925.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoTools#157:
       none
    2. `gh pr list --repo opensoft/openRepoTools --state all --search "#157"`, kept where the row or its body names `#157`:
    97	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	feat/crash-consistent-lane-worktree-recovery	MERGED
    3. `git ls-remote --heads git@github.com:opensoft/openRepoTools` branches naming `157` as a whole token:
       none
    
  5. brettheap commented on Oct 21, 2026

    @brettheap
    ContributorAuthor

    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).

    Nothing is left open here.

    Lane: openRepoTools-4

  6. brettheap commented on Oct 21, 2026

    @brettheap
    ContributorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions