Skip to content

Session relaunch: state which refusals are recorded and what a dry run creates; cite the mechanisms' vendor sources (#1116 follow-up d) - #1145

Merged
seathatflowsinourveins merged 1 commit into
mainfrom
claude/cc-native-practice-relaunch-p3s-20261015
Oct 16, 2026
Merged

seathatflowsinourveins merged 1 commit into
mainfrom
claude/cc-native-practice-relaunch-p3s-20261015

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Scope

  • What this PR changes, in one or two sentences: Session relaunch after an unplanned WSL restart: a roster timer and a boot oneshot on the systemd user manager #1116 follow-up (d), the remaining P3s from the GPT fix3 read and the command center's micro. The session-relaunch README and decision now state exactly which refusals are recorded and what a dry run creates. Each mechanism the tool relies on cites its vendor source, kept separate from local policy. The PublicationSurfaces scope is stated.
  • Base commit: e1b5288051292cfdb19e2ca64349e4da35ba0970 (the branch's parent; the first round was written against 0f4ee075b)
  • Lane: lane:foundation
  • Owned paths touched:
    • tools/session-relaunch/README.md;
    • docs/decisions/2026-10-14-session-relaunch-after-wsl-restart.md;
    • evidence/artifacts/session-relaunch-20261014/README.md (the test count);
    • tests/test_session_relaunch.py;
    • manifests/evidence.json.
    • session_relaunch.py is unchanged.
Item Source Change Test (failed before)
1 install read P3 The README's "a dry run records nothing" was wrong. A dry run takes the run lock, creates relaunch.lock, and creates the state directory (mode 0700) when it is missing. test_a_dry_run_creates_the_run_lock_and_nothing_else measures both a missing state directory and one holding only a roster
2 micro P3-1 The README names the two refusals that are recorded (only where a state file exists and parses, never in a dry run) and the five that only print and exit 1 test_only_the_refusals_the_readme_names_are_recorded drives each branch through main()
3 micro P3-2 The decision's "refuses with a record" now carries the same condition test_the_decision_records_a_reopen_refusal_only_where_a_state_exists
4 micro P3-3 The test comment cites the GPT read of #1116's follow-up at 7aad5a0 by commit and finding (its P2) instead of files outside the repository; 5937c61 removed the digest the first commit had cited text only
5 GPT fix3 P3-2 A README section, "Mechanisms: vendor behaviour and local policy", and a decision bullet cite flock, subprocess.run timeouts and os.replace test_the_readme_and_the_decision_cite_the_vendor_sources_of_the_three_mechanisms
6 GPT fix3 P3-5 The README's Tests section states that PublicationSurfaces scans 14 files test_the_readme_states_the_files_this_check_scans ties the number to the scanned list

The GPT fix3 P3-3 (a dry-run reopen writing audit state) was closed at 6bd6d99. Its P3-4 items are #1140 and #1141, or settled design.

SOTA sources

The unit runs /usr/bin/python3 3.14.4. Each Python sentence was checked in CPython Doc/library/*.rst at tag v3.14.4.

  • fcntl.flock: "See the Unix manual flock(2) for details." "If the flock() call fails, an OSError exception is raised."
  • flock(2) (Linux man-pages 6.19, 2026-02-08): an advisory lock on the open file. LOCK_NB makes it nonblocking. The lock is released by LOCK_UN, or when all duplicate descriptors have been closed.
  • subprocess.run: "If the timeout expires, the child process will be killed and waited for."
  • os.replace: "If successful, the renaming will be an atomic operation (this is a POSIX requirement)." The README says "atomic replacement", and it does not claim durability across a crash, because write_atomic does not fsync.

Evidence-class table

Claim Evidence class Command / receipt
A dry run creates exactly relaunch.lock (and the state directory when missing) local_integration test 1 against the real tool on a scratch state directory
Which refusals are recorded local_integration test 2 through main() on each branch
The docs sentences source_review the URLs above, plus CPython v3.14.4 rst

Local commands run

$ python3 -m unittest tests.test_session_relaunch        # each new test was also run with the docs at 0f4ee075: FAILED
Ran 56 tests; OK                                        # also under /usr/bin/python3 3.14.4
$ python3 scripts/validate.py
rc 0; {"components": 71, "hashed_files": 12040, "profiles": 4, "receipts": 240, "status": "passed"}

Notes for the reads

This PR, #1140 and #1141 each change the decision's Tests line, the evidence README's test count and manifests/evidence.json from the same base. The second and third to land need a count and registry rebase. The count is countTestCases(), 60 once all three are in.

Follow-ups

  • CC resolution check at 5937c61, P3: the decision's Tests line at docs/decisions/2026-10-14-session-relaunch-after-wsl-restart.md:63 reads "partial; The follow-up", a capital letter after a semicolon.

  • CC DELTA at 08c1878 (PASS):

    • F1 P3 tests/test_session_relaunch.py:733: the existing-parent check compares self.tmp with itself, and mkdtemp made it 0700, so the claim at session_relaunch.py:71 is untested. Pre-set a parent 0755.
    • F2 P3 tools/session-relaunch/README.md:102-104 (pinned by the test at :578-580): the README omits the missing parents a dry run also creates, now 0700.
    • F3 P3 docs/decisions/2026-10-14-session-relaunch-after-wsl-restart.md:63 says 56 unit tests and the evidence README says 58; the test at :845-850 pins only the README.
    • F4 P3 session_relaunch.py:70 and the test at :721 cite pathlib.rst at v3.13.16, not the pinned 3.14.4 (README:123; AGENTS.md Corrections).
    • F5 P3 the test at :722 claims every created directory is 0700, but handoff_dir's missing parents get the umask mode (session_relaunch.py:91).
    • F6 P3 session_relaunch.py:73-79 chmods a regular file at the path to 0700, where the base raised FileExistsError.
  • GPT full1 P3 (full1-pr1145/GPT-VERDICT-bae41b922.md): exact upstream captures are missing from the packet.

  • GPT full1 P3: clarify the inherited meaning of "skipped" in the exit-status description.

  • GPT fix1r read at 5937c61 (coordination/cc-reads-20261005/fix1r-pr1145/GPT-VERDICT-5937c61c5.md, 2026-10-15):

    • its P1 was this body, which named a private review by a shortened digest; the reference is now replaced by the read's PR,
      commit and finding (body edit, 2026-10-15). Residuals that no edit removes:
      • the body's edit history keeps the old revision (userContentEdits; deleting a revision is a UI-only act of the owner);
      • the branch's first commit carried that digest in its message and in a test comment. At the command center's ruling of
        2026-10-15 the branch was rebuilt as one commit, 6546ca0, with the earlier head's tree unchanged (tree 2687d490 before and
        after), and force-pushed with a lease; the earlier head stays only as a local backup ref, never pushed. GitHub may keep
        the earlier commits reachable from this pull request's timeline. The landing composes its squash message from a file
        (SQUASH_BODY_FILE), so no branch message reaches main;
    • follow-ups: extend the publication check to the PR-body input with shortened-digest coverage, tested with synthetic data
      only; the decision's test count (58, the module has 60) with the private_dir description and the old 56-test result marked
      historical; exact upstream captures unavailable; the directory-boundary items above; the inherited "skipped" exit-status
      wording.
    • Session relaunch: in-repo install and automated inverse, and background sessions by claude respawn #1187 (the session-relaunch install and inverse) edits the same tool files; whichever lands second rebases.

Checklist

  • No GitHub Actions changed.
  • No workflows changed.
  • No secrets are printed, logged or committed.
  • No new paid hosting, subscription or billing surface.
  • Peer-owned untracked files and worktrees were preserved.

🤖 Generated with Claude Code

@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Oct 15, 2026
seathatflowsinourveins added a commit that referenced this pull request Oct 15, 2026
…assert each refusal's reason

The command center's FULL read of #1145 at bae41b9 (CHANGES_REQUESTED):
- P1, digest custody: a test comment (tests/test_session_relaunch.py:484) carried the full sha256 of a GPT
  review file outside the repository. It now cites the read by PR, commit and finding only.
  test_the_tool_and_its_tests_publish_no_full_digest scans the tool, its tests and the evidence README; the
  base test file has 1 match.
- P3 (R:69): Path.mkdir(parents=True) created missing parents at the default mode (python/cpython v3.13.16
  cbc944f4 Doc/library/pathlib.rst:1476-1478). private_dir() now creates each missing directory 0700 (XDG Base
  Directory 0.8), and an existing parent keeps its mode. Fail-before:
  test_missing_parents_of_the_state_directory_are_created_private, 493 != 448.
- P3 (T:510): the dry-run comment said a dry run writes nothing. A dry run records no refusal and may create the
  run lock (test_a_dry_run_creates_the_run_lock_and_nothing_else).
- P3 (T:601): each --reopen case now asserts which refusal's reason fired, not only exit 1.

After: tests.test_session_relaunch 58 tests OK (the evidence README's count updated from 56); validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 15, 2026
…assert each refusal's reason

The command center's FULL read of #1145 at bae41b9 (CHANGES_REQUESTED):
- P1, digest custody: a test comment (tests/test_session_relaunch.py:484) carried the full sha256 of a GPT
  review file outside the repository. It now cites the read by PR, commit and finding only.
  test_the_tool_and_its_tests_publish_no_full_digest scans the tool, its tests and the evidence README; the
  base test file has 1 match.
- P3 (R:69): Path.mkdir(parents=True) created missing parents at the default mode (python/cpython v3.13.16
  cbc944f4 Doc/library/pathlib.rst:1476-1478). private_dir() now creates each missing directory 0700 (XDG Base
  Directory 0.8), and an existing parent keeps its mode. Fail-before:
  test_missing_parents_of_the_state_directory_are_created_private, 493 != 448.
- P3 (T:510): the dry-run comment said a dry run writes nothing. A dry run records no refusal and may create the
  run lock (test_a_dry_run_creates_the_run_lock_and_nothing_else).
- P3 (T:601): each --reopen case now asserts which refusal's reason fired, not only exit 1.

After: tests.test_session_relaunch 58 tests OK (the evidence README's count updated from 56); validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/cc-native-practice-relaunch-p3s-20261015 branch from 08c1878 to 5937c61 Compare October 15, 2026 14:05
…n creates; cite the mechanism sources; private parents

The remaining P3s on tools/session-relaunch after #1116 (follow-up d), and the command center's FULL read of this
change. One commit: the branch was rebuilt on 2026-10-15 at the command center's ruling, with the tree of the earlier
head unchanged, so that no commit on it carries a digest of an unpublished review file.

- README: a dry run is not write-free. Like every run it takes the run lock, and run_lock() opens relaunch.lock with
  O_CREAT (creating the state directory when it is missing, mode 0700). A dry run on a fresh state directory adds
  exactly relaunch.lock and records no refusal.
- README (CC micro P3-1): only two kinds of refused --reopen are recorded (a name that is not unresolved or not still to
  be relaunched; any --reopen while there is nothing to relaunch), and only where the ended boot's relaunch state already
  exists and parses, never in a dry run. plan()'s own refusals (no journal entry, an unreadable state), a missing or
  changed sealed handoff, a held run lock and a client that gives no list record nothing. Each --reopen case asserts
  which refusal's reason fired.
- Decision (CC micro P3-2): the 307f686 sentence no longer says the refusal is recorded unconditionally; the 7aad5a0
  sentence says a dry run records no refusal.
- Test comment (CC micro P3-3): the GPT read of #1116's follow-up at 7aad5a0 is cited by PR, commit and finding (its
  P2; the reviewer's two regression cases are the subtests case=complete and case=capped), not by files outside the
  repository. test_the_tool_and_its_tests_publish_no_full_digest scans the tool, its tests and the evidence README.
- README and decision (GPT fix3 P3-2): pinned vendor sources for the three mechanisms, kept apart from the tool's own
  policy: fcntl.flock (https://docs.python.org/3.14/library/fcntl.html#fcntl.flock) and flock(2)
  (https://man7.org/linux/man-pages/man2/flock.2.html, Linux man-pages 6.19); subprocess.run(timeout=...)
  (https://docs.python.org/3.14/library/subprocess.html#subprocess.run); os.replace
  (https://docs.python.org/3.14/library/os.html#os.replace). The unit's interpreter is /usr/bin/python3 3.14.4; the
  cited sentences are the same in CPython's Doc/library/ at tag v3.14.4. write_atomic never fsyncs, so the README
  claims atomic replacement, not durability.
- README Tests (GPT fix3 P3-5): PublicationSurfaces scans 14 files (the decision and every file under
  tools/session-relaunch/ and the evidence folder); a test ties the stated count to the scanned list.
- Private parents (CC FULL read, P3): Path.mkdir(parents=True) created missing parents at the default mode
  (python/cpython v3.13.16, cbc944f4, Doc/library/pathlib.rst:1476-1478). private_dir() now creates each missing
  directory 0700 (XDG Base Directory 0.8), and an existing parent keeps its mode.

Tests: tests.test_session_relaunch 60 OK at this tree; validate.py passed after re-registering the edited files in
manifests/evidence.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/cc-native-practice-relaunch-p3s-20261015 branch from 5937c61 to 6546ca0 Compare October 15, 2026 23:39
@seathatflowsinourveins
seathatflowsinourveins merged commit 8dfe8b5 into main Oct 16, 2026
34 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/cc-native-practice-relaunch-p3s-20261015 branch October 16, 2026 00:43
seathatflowsinourveins added a commit that referenced this pull request Oct 16, 2026
…st guards it

The GPT fix2 read of #1141 at 07ea567 (2026-10-15, CHANGES_REQUESTED, one P2 blocker): the claim that the carry
regression fails without the re-carry line was false at that head. The fold at 07ea567 made a row leave the carry only
when the final list shows its session running, so an absent session's row never left, and the re-carry line that added
it back had become redundant: removing either the line or the retire rule's running-only selection left the test
passing, since each covered the other.

- tools/session-relaunch/session_relaunch.py: the redundant re-carry line is removed; the final read's retire rule
  (running only) is the one mechanism that keeps an absent session's row carried.
- tests/test_session_relaunch.py: test_a_session_that_registers_and_then_exits_is_carried_again now states what it
  guards. With the retire rule mutated to retire absent rows too, it fails; on this tree it passes (a mutated copy
  in a scratch directory, no repository file changed).

The branch is rebuilt onto main at the command center's ruling of 2026-10-16: the earlier history held two commits that
landed as #1145 in another form; this branch keeps only its own three commits, whose messages are unchanged, and this
fold. The GPT read's adjacent P2s (a stale live sample after finalization, a manually recovered lane carried again, a
same-boot retry reopening recovered lanes) and its P3s are Follow-ups in the body.

After: tests.test_session_relaunch 66 OK; validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 16, 2026
…st guards it

The GPT fix2 read of #1141 at 07ea567 (2026-10-15, CHANGES_REQUESTED, one P2 blocker): the claim that the carry
regression fails without the re-carry line was false at that head. The fold at 07ea567 made a row leave the carry only
when the final list shows its session running, so an absent session's row never left, and the re-carry line that added
it back had become redundant: removing either the line or the retire rule's running-only selection left the test
passing, since each covered the other.

- tools/session-relaunch/session_relaunch.py: the redundant re-carry line is removed; the final read's retire rule
  (running only) is the one mechanism that keeps an absent session's row carried.
- tests/test_session_relaunch.py: test_a_session_that_registers_and_then_exits_is_carried_again now states what it
  guards. With the retire rule mutated to retire absent rows too, it fails; on this tree it passes (a mutated copy
  in a scratch directory, no repository file changed).

The branch is rebuilt onto main at the command center's ruling of 2026-10-16: the earlier history held two commits that
landed as #1145 in another form; this branch keeps only its own three commits, whose messages are unchanged, and this
fold. The GPT read's adjacent P2s (a stale live sample after finalization, a manually recovered lane carried again, a
same-boot retry reopening recovered lanes) and its P3s are Follow-ups in the body.

After: tests.test_session_relaunch 66 OK; validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 16, 2026
… partial run's unfinished rows (#1116 follow-ups b, c) (#1141)

* Session relaunch: lock the roster against the timer snapshot; carry a partial run's unfinished rows

(c8d7fa9 :512 and :221-223; the co-op's local review of 7aad5a0, by name only). One PR, since
both concern what this boot's roster carries.

- (b) A timer snapshot that read its carry before a starting relaunch saved its
  state, and wrote after the relaunch's seed roster, replaced the seed with the
  live list only. roster_lock() (flock on roster.lock in the state directory)
  is now held by the snapshot from its carry read to its roster write
  (snapshot_live) and by the relaunch while it saves its state and then writes
  the seed. Both holders are short; relaunch.lock stays the run's own lock.
- (c) carried_rows() carried only an in_progress state's rows, so the first
  snapshot after a partial run dropped the lanes the relaunch had not brought
  back and a second crash lost them. A partial state of this boot carries them
  too (minus the ones listed live).
- README: the roster paragraph. The decision's test line and the evidence
  README count: 53.

Fail-first: both new tests failed on c8d7fa9 (the seed replaced by an empty
roster; the partial run's rows missing); with the lock disabled and only the
reorder kept, the race test still fails. The module runs 53 tests OK. Own
logic; flock(2) semantics as the existing relaunch.lock uses them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Session relaunch: write the seed roster before the state; retire carried rows seen running; cite flock

Fix round for #1141 at 474cf2b:
- GPT read (P2, reproduced): a boot stop between save() and the seed roster left the timer's empty roster
  of this boot as the newest, and the next boot opened nothing. cmd_snapshot now runs before save(), both
  still under roster_lock (the reviewer's seed-first control).
- Local review P2 (:244-245): the carry was taken before any tab opened, so a lane the run brought back
  stayed carried and was listed again after it exited on purpose. retire() drops a row from the carry once
  the run sees its session running (after each tab and in the final list).
- Local review P2 (:153-156): roster_lock and the README cite fcntl.flock at python/cpython v3.13.16
  (cbc944f4) Doc/library/fcntl.rst:176-183 and release-on-close at Linux man-pages-6.9.1
  man/man2/flock.2:69-72; both sources added to receipts/sources.json.

Fail-before (base code, new tests): test_a_boot_stop_right_after_the_state_write_still_relaunches_from_the_seed,
Lists differ: [] != ['cc', 'trading-cc', 'coop']; test_a_lane_brought_back_by_a_partial_run_leaves_the_carry,
{'wsl-architecture-design', 'ns2604-coop'} is not false. After: tests.test_session_relaunch 55 tests OK
(the evidence README's count updated from 53); validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Session relaunch: retire a carried row only when the final list shows its session running

The command center's FULL read of #1141 at b2755a0 (P2, a lane can be dropped silently): a row left the carry the
first time its session was seen running, so when the command center registered and then exited while the next tab
opened, and the boot stopped before the final pass, every snapshot wrote this boot's roster with it neither live
nor carried. The per-tab retire is removed; the final pass retires only sessions its live list shows running, and
an absent session re-enters the carry (the rebase resolution).

test_a_session_that_registers_and_exits_before_a_crash_stays_carried, fail-before at b2755a0:
'wsl-architecture-design' not found in the roster. P3s: the tool README states the final-list rule and that an
absent row re-enters the carry; the decision's and the evidence README's test counts are 66.
After: tests.test_session_relaunch 66 tests OK; validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Session relaunch: one carry mechanism at the final read; the carry test guards it

The GPT fix2 read of #1141 at 07ea567 (2026-10-15, CHANGES_REQUESTED, one P2 blocker): the claim that the carry
regression fails without the re-carry line was false at that head. The fold at 07ea567 made a row leave the carry only
when the final list shows its session running, so an absent session's row never left, and the re-carry line that added
it back had become redundant: removing either the line or the retire rule's running-only selection left the test
passing, since each covered the other.

- tools/session-relaunch/session_relaunch.py: the redundant re-carry line is removed; the final read's retire rule
  (running only) is the one mechanism that keeps an absent session's row carried.
- tests/test_session_relaunch.py: test_a_session_that_registers_and_then_exits_is_carried_again now states what it
  guards. With the retire rule mutated to retire absent rows too, it fails; on this tree it passes (a mutated copy
  in a scratch directory, no repository file changed).

The branch is rebuilt onto main at the command center's ruling of 2026-10-16: the earlier history held two commits that
landed as #1145 in another form; this branch keeps only its own three commits, whose messages are unchanged, and this
fold. The GPT read's adjacent P2s (a stale live sample after finalization, a manually recovered lane carried again, a
same-boot retry reopening recovered lanes) and its P3s are Follow-ups in the body.

After: tests.test_session_relaunch 66 OK; validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Session relaunch: a recovered lane stays recovered; snapshot sampling and the final publication share roster.lock

The fold of the rebuild of #1141 on main fa6cf1a (the command center's rulings of 2026-10-16T03:41Z and 04:39Z). It
answers the co-op's local review of #1141 at aedf554 (P2, :255-256) and the GPT read res1 at aedf554
(GPT-VERDICT-aedf55498.md: follow-ups 2 to 4 joined as one family, and P3s 5 and 7).

- One persisted retirement. A run's final read, and a snapshot after a run that ended partial, record the lanes they see
  running as `retired` in the relaunch state and drop their rows from its carry (retire_into, retire_recovered). A
  retired lane is not carried again (follow-up 3), and a rerun of the same relaunch neither opens it nor carries it: plan()
  leaves it out of the launch set, and the rerun's carry skips it (follow-up 4). A --reopen of a retired lane is refused;
  its own launcher starts it again. A session that registered and then went missing from the final list (absent) is not
  retired and stays carried, as before.
- One synchronization point (follow-up 2). The timer's snapshot samples the live list under roster.lock, and a run
  publishes its final outcomes, retirement and status under the same lock. So a snapshot never writes a list sampled
  before that publication with the carry read after it.
- The roster race test waits until the relaunch reaches the lock, not a one-second sleep (P3 5). The README carry
  wording is updated (P3 7).

Tests: tests/test_session_relaunch.py, 69 (2 new, 2 changed). On aedf554's production code
(fail-first-r2.txt), each of these fails on behaviour:
- the snapshot sampled before the final publication loses the three recovered lanes;
- a rerun opens cc, trading-cc and coop instead of trading-cc;
- the next boot relaunches a lane that recovered late and then stopped.
The handshake race test passes there and on this head. validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Session relaunch: every read-modify-write of the relaunch state under roster.lock; a retired session is satisfied

The commit on ca89139 for the co-op's local review at ca89139 (pr1141-ca891398/review_output.json, two P2s in the
relaunch-lifecycle family; the command center's ruling of 2026-10-16T05:47Z: one normal commit, no rebuild).

- P2 (session_relaunch.py:217-220), every state read-modify-write. plan() read the state before roster.lock, and
  record_refusal() never took the lock, so two interleavings lost a write:
  - a snapshot's retirement was erased by a rerun's stale save;
  - a refusal record was saved over by a snapshot.
  The fix:
  - prepare_run() runs a run's decision and first writes under roster.lock, from plan()'s read to the seed roster and
    the first save; a refused --reopen is recorded inside that span;
  - a dry run writes nothing and takes no roster.lock;
  - a rerun retires the carried lanes it finds live at its start, as a snapshot after that run would, so the retirement
    no longer depends on which of the two ran first.
- P2 (:647-649), completion. A rerun that brought back every lane still owed stayed partial while the lanes it had
  retired earlier were stopped on purpose; main() then exited 1, and further reruns spent the crash-loop budget. A
  retired session now counts as satisfied, and is not marked absent at the final read.

Tests: tests/test_session_relaunch.py, 72 (3 new). Each fails on ca89139's code (fail-first-r3.txt):
- a snapshot started right after plan()'s read: the command center's retirement is erased;
- a snapshot holding its copy while a --reopen is refused: the refusal record is lost (0 != 1);
- a rerun that brings back trading-cc: it stays partial.
validate rc 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Session relaunch: roster.lock reentrant in one thread; a rerun's plan reconciled with retirements; refusals locked

The commit on 3436a5b. It answers GPT res2 at ca89139 (res2-pr1141-ca8913985/GPT-VERDICT-ca8913985.md): its blocker
(:548/:588/:617) and its refusal-path follow-up (:526/:532/:538). It folds the FRONT micro at 3436a5b
(fix3-pr1141-3436a5b03/GPT-VERDICT-3436a5b03.md, ACK, 0 blockers): P3 3 here, and P3 2 in the body.

Before this commit, res2's retained probe (review-results/test_relaunch_adjacent.py) ran against 3436a5b as follows:
- completion and lock exclusion: OK;
- the refusal case: FAIL. record_refusal() called on its own rewrote a stale state, because only its caller held the
  lock;
- the stale-plan case: HANG at 60 s. A snapshot injected inside plan(), in the same thread, waited on the
  roster.lock that the run already holds.

The fix:
- roster_lock is reentrant within one thread (a thread-local depth). A nested hold takes no second flock, while another
  thread or process still waits.
- prepare_run reconciles the plan with the state on disk: lanes retired while it planned are dropped from the launch,
  and the run continues from that state.
- record_refusal holds roster_lock itself and appends to the state as read at the write.
- The README's completion paragraph (:77) now counts a retired session as satisfied, as the rule and code do.

The probe at this head: 4 of 4 OK.

Tests: tests/test_session_relaunch.py, 75 (3 new). Each fails on 3436a5b's code as a failure, not a hang
(fail-first-r4.txt):
- the rerun waits on its own roster.lock;
- the refusal loses trading-cc's retirement;
- a nested hold in the same thread waits on its own lock.
validate rc 0. The source-capture gap (micro P3 1, res2 finding 4) stays a Follow-up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant