Skip to content

Session relaunch after an unplanned WSL restart: a roster timer and a boot oneshot on the systemd user manager - #1116

Merged
seathatflowsinourveins merged 5 commits into
mainfrom
claude/cc-native-practice-session-relaunch-20261014
Oct 15, 2026
Merged

seathatflowsinourveins merged 5 commits into
mainfrom
claude/cc-native-practice-session-relaunch-20261014

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Scope

  • What this PR changes: the core Claude sessions come back by themselves after an unplanned WSL restart.
    • A systemd --user timer writes the live sessions (claude agents --json) to a per-boot roster every minute.
    • A boot oneshot reads the newest roster from an earlier boot. Unless that boot ended in a planned restart, it writes one sealed handoff and opens a fresh tab per profiled session, command center first, through the command center's existing open-tab.ps1 and launch.sh.
    • relaunch-<ended boot>.json records each run and each session's outcome. An unfinished relaunch is rerun with the same handoff and opens only the sessions not running; a crash before the timer's first snapshot is recovered by the next boot.
    • Nothing is installed: the install, check and inverse are in tools/session-relaunch/README.md, and installing is the command center's act.
  • Why: WSL stopped at 06:41:17Z on 2026-10-14. The distro started again at 12:48:57Z, and no session came back until the command center relaunched them by hand at 12:53:44-12:54:12Z. At 08:50 ET on 2026-10-14 the owner told the command center that the sessions had not come back by themselves.
  • Base commit: 8c11dac090f71d340abd54abf8c67ffae13fa2d5
  • Lane: lane:foundation
  • Owned paths touched (all new except the manifest):
    • tools/session-relaunch/: session_relaunch.py, README.md, config.example.json, units/
    • tests/test_session_relaunch.py
    • docs/decisions/2026-10-14-session-relaunch-after-wsl-restart.md
    • evidence/artifacts/session-relaunch-20261014/
    • manifests/evidence.json

SOTA sources

The relaunch builds on the layer's recorded supervisor and the client's own interfaces; nothing new is installed.

  • Layer verdict: catalogs/landscape/foundation.json line 3376, scheduling-supervision. Current choice: "Dagu for selected local workflows and systemd user-manager supervision". Winners: systemd 255.4-1ubuntu8.17 and dagu 2.16.6. The host runs systemd 259.
  • Claude Code 2.1.299, the installed client:
    • claude agents --help, lines 24-25: "--json Print active sessions (interactive and background) as a JSON array and exit (for scripting; does not require a TTY)".
    • claude daemon --help, line 12: "Service install is disabled in this version — the daemon runs on demand".
  • Agent-view docs: https://code.claude.com/docs/en/agent-view.md, read 2026-10-14T12:58:26Z, sha256 c8e2b81092b94b4b3ded2c86b40b8329e73b8f39b36acb3288bd56385bc83266.
    • Line 884: "Shutting down or restarting your machine stops running background sessions."
    • Line 886: "Within 48 hours, the session shows as failed. Attach or reply to it and it restarts from its saved conversation."
    • Line 982: "They are preserved across sleep but stop if the machine shuts down."
    • So native background sessions do not cover this case.
  • CHANGELOG: https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md, sha256 7abf62a9…, line 394 (2.1.295): claude agents stopped "(macOS, or Linux without the service installed): running sessions now stop in about a minute".
  • Earlier decision: docs/decisions/2026-09-28-terminal-experience.md line 616. Windows Terminal's persistedWindowLayout starts a fresh process from each tab's saved command line and reattaches nothing.
  • Practice:
    • the claude-native-practice skill's scheduled-tasks slot, which defaults to systemd --user units. Its note: "after a shutdown nothing fires until the distro starts".
    • the command center's fresh-from-handoff relaunch (launch.sh).

Evidence-class table

Claim Evidence class Command / receipt
A systemd --user unit runs powershell.exe (rc 0, Windows session 1) without WSL_INTEROP native_proven (inside a running boot) evidence/artifacts/session-relaunch-20261014/receipts/probes.json interop-from-user-unit
A tab opened from such a unit runs a process back in the distro about 1 s later native_proven (inside a running boot) same, tab-from-user-unit; probe/tab_probe.ps1, probe/tab_probe.sh
claude agents --json costs 0.18-0.23 s per call native_proven same, agents-json-cost
~/.claude/sessions/ kept no file for any of the 7 sessions alive at the stop; pidDomain is the machine id native_proven same, session-files-after-the-stop
Native background sessions restart after a shutdown only on attach or reply source_review receipts/sources.json
The 06:40:30Z roster relaunches six sessions, command center first, and leaves one to the command center synthetic (simulation on the real journal boots) probe/retro_dry_run.py, receipts/retro-handoff.md
Decision rules, handoff, tab calls, retries, reruns of an unfinished relaunch, refusals, file modes, crash-loop cap synthetic (stand-in client and PowerShell) tests/test_session_relaunch.py (51 with PublicationSurfaces)
No published sentence of this change attributes a quoted phrase to the owner local_integration (structural check; fails at the first read head 7a504750) tests/test_session_relaunch.py PublicationSurfaces
A relaunch at a real boot not established waits for the first restart after the install; relaunch-<boot>.json records each call and outcome

Local commands run

$ systemd-run --user --wait --pipe --collect ... powershell.exe ... Write-Output interop-ok   # probes.json
unit lacks WSL_INTEROP / interop-ok / 1 / rc=0
$ systemd-run --user --wait --pipe --collect ... powershell.exe -File tab_probe.ps1
wt_pid=115388 session=1; the tab wrote "2026-10-14T13:05:46Z tab-probe boot=beb4ce83..." and closed
$ python3 -m unittest tests.test_session_relaunch   # against 9f0127c8's module, README and config, round 2's tests
Ran 38 tests, FAILED (failures=10, errors=10)
$ python3 -m unittest tests.<module>   # at 376f2bb9, one process per module, idle-class, 20:49:08-20:50:57Z
rc 0 for all 7: test_session_relaunch (38), test_local_pages_policy_grants (25), test_local_pages_architecture_policy (11), test_grand_catalog_counts (17), test_runtime_worker_openhands_push_gate (127), test_evidence_manifest (5), test_validate (86)
$ python3 scripts/validate.py   # at 376f2bb9
rc 0, status passed, 12,024 hashed files
$ python3 -m unittest tests.test_session_relaunch   # against 376f2bb9's module, README and unit, the blocker commit's tests
Ran 45 tests, FAILED (failures=8, errors=1)
$ python3 -m unittest tests.<module>   # at 307f686c, one process per module, idle-class, 23:46:02-23:50:31Z
rc 0 for all 7: test_session_relaunch (45), test_local_pages_policy_grants (25), test_local_pages_architecture_policy (11), test_grand_catalog_counts (17), test_runtime_worker_openhands_push_gate (127), test_evidence_manifest (5), test_validate (86)
$ python3 scripts/validate.py   # at 307f686c
rc 0, status passed, 12,024 hashed files
$ python3 -m unittest tests.test_session_relaunch   # against 307f686c's module, the item-2 commit's tests
Ran 49 tests, FAILED (failures=7)   # the four new tests (6 failures across subtests) and the count check
$ python3 -m unittest tests.<module>   # at 7aad5a02, one process per module, idle-class, 00:32:35-00:35:41Z
rc 0 for all 7: test_session_relaunch (49), test_local_pages_policy_grants (25), test_local_pages_architecture_policy (11), test_grand_catalog_counts (17), test_runtime_worker_openhands_push_gate (127), test_evidence_manifest (5), test_validate (86)
$ python3 scripts/validate.py   # at 7aad5a02, and at origin/main a339425c for comparison
rc 1 at both, the same nine model-currency lines (a landscape check older than 7 days after the UTC date change); no line names this change
$ python3 -I evidence/artifacts/session-relaunch-20261014/probe/retro_dry_run.py <module> <config> <out> <rows.json>
the same plan (six relaunched, one left); the handoff is byte-identical to receipts/retro-handoff.md

Decision record

docs/decisions/2026-10-14-session-relaunch-after-wsl-restart.md. It covers the alternatives (background sessions, persistedWindowLayout, the session files, resume, tmux) and what overturns each.

Fix round 1 (one rewritten commit, 9f0127c8, replacing 7a504750): the first read's blocker 1

  • Astra's first read: CHANGES_REQUESTED, with blocker 1 (privacy class) and four P2s. Verdict file full1-pr1116-7a5047505/GPT-VERDICT-7a5047505.md, sha256 15703c8d.... It is cited by file and number; its shape stays in that file and is not restated.
  • Blocker 1 is fixed in the decision record, the evidence README and this description, which now carry a dated paraphrase.
    • The two registry rows are recomputed. No prefix of the retired digests remains in the diff, the commit message or this description.
    • On the command center's order, the commit replaces the PR's earlier commit.
    • PublicationSurfaces checks every published file of the change. It fails at 7a504750.
  • Registry: it now adds only this change's 15 rows (75 lines added and 0 removed against the base). The earlier commit's re-serialization of 13 unrelated lines (the read's item 10) is gone.

Fix round 2 (one commit, 376f2bb9, on 9f0127c8): the first read's four P2s and the local review

The co-op's privacy scan of 9f0127c8 was clean first. Against 9f0127c8's module, README and config, 20 of the 38 tests fail; the only new test that passes there is the guard that an existing handoff directory keeps its mode. Seven of eight one-line mutations of the fixes failed the module at once, and the eighth got its own test.

  • P2-2, a failed session query (the local review's first P2): list_sessions returns None instead of an empty list. snapshot then keeps the last good roster and exits 1. relaunch asks again for up to client_wait_s, then refuses: it writes and opens nothing, and exits 1. Tests: test_a_failing_client_gives_no_list_and_an_empty_one_stays_a_list, test_a_failing_client_keeps_the_last_good_roster_until_it_answers_again, test_an_unanswering_client_refuses_the_relaunch_and_opens_nothing_until_it_answers, test_a_client_answering_within_the_wait_is_used.
  • P2-3, the marker before any tab (and the local review's crash before the first snapshot):
    • relaunch-<boot>.json replaces the relaunched marker. It holds the status (in_progress, partial or complete), each run, and each session's calls and outcome (pending, running, failed, uncertain or unconfirmed).
    • Only complete stops a relaunch, and it is set only when every session is listed. A rerun reuses the sealed handoff (refused if its sha256 differs) and opens only the sessions not running. The cap counts runs.
    • A run first writes this boot's roster as the live sessions plus the ended boot's rows not running yet. The minute snapshot carries those rows while the run is in_progress.
    • Tests: test_a_run_stopped_before_its_first_tab_is_rerun_with_the_same_handoff, test_failed_tabs_are_retried_by_a_rerun_and_only_the_sessions_not_running, test_a_rerun_after_every_session_came_back_by_hand_completes_without_a_tab, test_a_crash_before_the_first_minute_snapshot_is_recovered_by_the_next_boot, test_the_minute_snapshot_carries_the_unrelaunched_rows_while_a_relaunch_runs, test_a_completed_relaunch_is_not_run_again_and_an_unfinished_one_is, test_the_cap_counts_relaunch_runs_in_the_window.
  • P2-4, a retry after an indeterminate call: a call that times out after tab_timeout_s is never repeated. The run waits for the session (settle_s; cc_wait_s for the command center) and records it as running or uncertain. Only a definite failure is retried, and only after the live list shows the session still absent. Tests: test_a_tab_that_launches_then_times_out_is_not_opened_again, test_a_tab_that_times_out_without_a_session_is_uncertain_and_not_retried, test_a_failed_tab_whose_session_is_listed_is_not_retried, test_a_tab_reported_opened_but_never_listed_is_unconfirmed.
  • P2-5, file modes: rosters, the state and the planned marker are 0600, and the handoff is 0400. The state directory and rosters/ are 0700, and the handoff directory is made 0700 only when the tool creates it. Tests: test_the_records_are_private_to_the_user (with the old :194 assertion now 0400), test_an_existing_handoff_directory_keeps_its_mode.
  • Local P2, missing boot history: with no journal entry for the ended boot, nothing is relaunched, because the roster's age cannot be checked. Test: test_without_the_ended_boot_in_the_journal_nothing_is_relaunched.
  • Local P3: the README's plan-restart command runs the installed tool through /usr/bin/python3 -I. Test: test_the_readme_runs_every_installed_command_through_python.
  • Unchanged: the signatures of cmd_snapshot, plan and handoff_text, and the handoff text. The retro dry run reproduces receipts/retro-handoff.md byte for byte.
  • Registry: only the five rows of the edited files change.

Fix round 3 (the blocker commit 307f686c on 376f2bb9): two paths to a second live session

The command center's adjudication of 376f2bb9 (2026-10-14T23:32:53Z) ruled the first read's P2-a and P2-b P1, because either one could open a second live session of one lane, and nothing (no lock, no session-id record) guarded against it. Its scope was one commit with these items:

  • F1, a rerun reopened an unconfirmed tab start: each call is recorded in flight before it is made, then replaced by its result. A rerun leaves an unconfirmed, uncertain or in-flight session as it is, because its first claude -n may still register. Only the command center reopens one, by name, with relaunch --reopen <name>, recorded under the run's reopened. Tests: test_a_rerun_does_not_reopen_a_tab_start_it_could_not_confirm (both outcomes) and test_a_run_stopped_during_a_tab_call_leaves_it_unresolved.
  • F2, a failed pre-open query on the first attempt opened a tab: a call is made only when the live list reads the session as absent. An unreadable list skips the session (skipped, nothing opened); a rerun may open it, since no call was made. Test: test_an_unreadable_live_list_before_the_first_call_opens_nothing.
  • F3, two roster rows of one name got two turns: one turn per name; the second row is left to the command center in the handoff. Test: test_two_roster_rows_of_one_name_get_one_turn.
  • F4, the journal-missing and unreadable-state skips exited 0: both are refusals with exit 1, and the relaunch unit gains OnFailure=stack-alert@%n.service, the handler adoption/templates/systemd/mineru.service uses (systemd.unit(5), OnFailure=). The README's install step confirms the handler is loaded. Tests: test_a_missing_journal_entry_or_an_unreadable_state_exits_nonzero, test_a_failed_relaunch_run_alerts.
  • README test count: the evidence README and the decision state 45. Test: test_the_evidence_readme_states_the_module_test_count.
  • Fail-first: against 376f2bb9's module, README and unit, the seven new tests fail (8 failures and 1 error; F1 fails in both its subtests). Two earlier tests that stopped a run at the tab call now stop it at the live-list read before the call, because a call stopped in flight is now unresolved by design and has its own test. Each of the seven fixes, reverted alone, fails the module.
  • Unchanged: the handoff text for a roster without repeated names; the retro dry run still reproduces receipts/retro-handoff.md byte for byte.
  • Registry: only the six rows of the edited files change.
  • Privacy scan: clean on the commit message. The added lines show two hits of the email shape, both the systemd instance name of the stack-alert template for this unit, a form recipes/mineru-local-service.md on main already carries.

Fix round 4 (one commit, 7aad5a02 on 307f686c): the item-2 invariant

The command center's DELTA read of 307f686c found items 1 and 3-7 done and two P1 gaps in item 2, both able to open a second live session of one lane. Its adjudication addendum (2026-10-15T00:11:23Z) allowed one more commit, scoped to that invariant:

  • P1-a, a run stopped in its settle wait: the call's result was saved while the outcome stayed pending, so a rerun opened the lane again. The result is now saved together with the provisional outcome (unconfirmed after rc 0, uncertain after a timeout) before the wait. A session is unresolved when a call since its last reopen opened, timed out or is in flight, whatever its outcome label. Test: test_a_run_stopped_in_its_settle_wait_is_not_reopened_by_a_rerun.
  • P1-b, a direct --reopen run overlapping the unit's run: every relaunch run holds an exclusive fcntl.flock on relaunch.lock in the state directory. A run that finds it held is refused, exit 1, writing and opening nothing. Test: test_a_held_run_lock_refuses_the_run_and_opens_nothing.
  • A --reopen run that makes no call: a reopen is spent only by the call it makes (resolved_through), so a skipped reopen leaves the lane unresolved and the next plain rerun opens nothing. Test: test_a_reopen_run_that_makes_no_call_leaves_the_lane_unresolved.
  • An invalid --reopen name: a name that is not an unresolved session this relaunch still has to bring back is refused, exit 1, with a refusals entry in the state file. Test: test_a_reopen_of_a_name_that_is_not_unresolved_or_not_planned_is_refused_with_a_record (a live lane, an unprofiled lane, an unknown name).
  • Fail-first: with 307f686c's module, the four new tests fail (6 failures across their subtests, plus the count check). The test file is byte-identical at that run and at this commit (sha256 a0d213c6…); the receipt is p1b-fail-before.txt, sha256 f8e00bb8…. Each of the five fixes, reverted alone, fails the module (p1b-mutants.txt, sha256 f9eabc36…).
  • Unchanged: the retro dry run still reproduces receipts/retro-handoff.md byte for byte.
  • Registry: only the five rows of the edited files change. The privacy scan is clean on the commit message and the 153 added lines.

Fix round 5 (registry-only rebase onto main, then one commit 6bd6d99d): the refusal record

The rebase has no other content change. 7aad5a02 was rebased onto main 929bc3c3 under the command center's standing rule: manifests/evidence.json was the only conflict, the 15 branch rows were re-registered, lg_change_id is 0c6c3bbe… before and after, merge-tree exits 0, and the rebased parent is 56f2fec4.

The GPT read of 7aad5a02 found no double-launch path; its P2 and P3 concern the refusal record. The command center ruled at 2026-10-15T01:50Z: one commit, no loop.

  • P2, a refused --reopen with no record: a --reopen refused because the relaunch is complete or capped returned before the only writer of the refusal record. record_refusal() now appends the entry to that boot's relaunch state under the run lock, keeping runs, sessions and earlier refusals, adding no run and making no tab call. With no readable relaunch state there is nothing to add it to; the README says so, and the refusal goes to stderr only. Test: test_a_reopen_refused_by_a_complete_or_capped_relaunch_is_recorded (both cases).
  • P3, dry run: the refusal write ran before the dry-run return; a dry run now records nothing. Test: test_a_dry_run_records_no_refusal.
  • Fail-first: at 56f2fec4 the new tests fail (p5-fail-before.txt, sha256 1528fd11…). The reader's eight regression cases (its test_review_paths.py, sha256 913b3afd…) pass at the new head, two of which failed before.
  • Unchanged: the retro handoff is byte-identical. All 7 modules exit 0 at 6bd6d99d, and so does validate.py (12,040 files).

Follow-ups

  • Install (the command center): follow tools/session-relaunch/README.md, including the inverse.
  • Distro start after a WSL service restart (the host lane, d4): without it, no linger unit runs until something calls wsl.exe.
  • Planned restarts: the command center's hands-free restart runs /usr/bin/python3 -I "$T/session_relaunch.py" plan-restart --config "$S/config.json" first, so only its own path relaunches.
  • First real restart after the install: read relaunch-<boot>.json for each call and outcome, then record the boot-time measurement. A partial run makes the oneshot exit 1, so it shows as failed in systemctl --user --failed; the README has the rerun.
  • The local review's other P2s (lines at 376f2bb9): the partial carry (:221), a stale running outcome (:452-458) and stale session ids (:428-429). Per the command center's read, they can lose or suppress a relaunch or cite a stale transcript; none opens a second tab.
  • The DELTA read of 7aad5a02 (PASS; no double-launch sequence found), its P3s and open checks:
    • P3-1: the "refused with a record" wording is not universal. A refusal at the run lock and on the first run of a relaunch goes to stderr only (:456-459).
    • P3-2: a dry run with an invalid --reopen still writes a refusals entry, because the reopen check (:462-471) runs before the dry-run return.
    • Two branches have no test of their own: the planned-but-resolved clause of the reopen check (:464), and a timed-out call that is saved as uncertain.
    • Unchecked on Windows: whether a killed run's powershell.exe outlives its WSL interop stub.
  • The local review's three P2s at 7aad5a02, in the command center's order:
    1. :533: reconcile the sessions before marking the relaunch complete. A lane that registers and then dies is left dead and unrecovered.
    2. :496: a cross-process roster lock between the seed roster and the timer's snapshot.
    3. :222: carry a partial run's unfinished rows.
  • The DELTA read's residual P2 (adjudicated design): a same-name session started by hand within the launch-to-listed latency, before the pre-call live-list read, still gets a second tab.
  • The DELTA read's P3 on the handoff: the dedupe sentence names two reasons only, and the deduplicated row offers claude --resume for a second session of a name relaunched above.
  • The first read's P3s 6-10, outside this round:
    • 6: the planned-marker semantics (a cancelled planned restart) and the first 90 s;
    • 7: TimeoutStartSec=900 against the retry and settle budget, and command-center readiness before the workers;
    • 8: the inverse leaves AUTO-RESTART-* files, and the install assumes ~/.config/systemd/user/;
    • 9: the launcher's source custody;
    • 10: locator churn in the registry.

Checklist

  • No GitHub Actions changed.
  • No workflows changed.
  • No secrets printed, logged or committed. Session ids are published as 8-hex prefixes only, and the full rows stay in the state directory.
  • No paid hosting, subscription or billing surface.
  • Peer-owned files and worktrees preserved.

🤖 Generated with Claude Code

@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Oct 14, 2026
@seathatflowsinourveins
seathatflowsinourveins marked this pull request as ready for review October 14, 2026 13:22
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/cc-native-practice-session-relaunch-20261014 branch 2 times, most recently from 7125fff to 9f0127c Compare October 14, 2026 20:25
seathatflowsinourveins added a commit that referenced this pull request Oct 14, 2026
…d journal refusals, no repeat of a timed-out tab, private modes

The first read of #1116 at 7a50475 asked for four P2 fixes; the local review found two of them and one more.

- A failed `claude agents --json` is no longer an empty roster. list_sessions returns None; snapshot keeps the
  last good roster and exits 1; relaunch asks again for up to client_wait_s, then refuses, writing and opening
  nothing.
- relaunch-<boot>.json replaces the relaunched marker: status in_progress, partial or complete, each run, and
  each session's calls and outcome (pending, running, failed, uncertain, unconfirmed). Only complete stops a
  relaunch. A rerun reuses the sealed handoff (checked by sha256) and opens only the sessions not running. A run
  first writes this boot's roster as the live sessions plus the ended boot's rows not running yet, and the
  minute snapshot carries those rows while the run is in progress, so a crash before the timer's first
  snapshot is recovered by the next boot. The cap counts runs.
- A timed-out tab call (tab_timeout_s) is an uncertain side effect: the run waits settle_s for the session and
  never calls again. Only a definite failure is retried, after the live list shows the session still absent.
- Rosters, the state and the planned marker are 0600, the handoff 0400, the state directory and rosters 0700;
  the handoff directory is made 0700 only when the tool creates it.
- No journal entry for the ended boot is a refusal: the roster's age at that boot's end cannot be checked.
- The README's plan-restart command runs the installed tool through /usr/bin/python3 -I.

Tests: 38, 16 of them new and 6 changed. Against 9f0127c's module, README and config, 20 fail. Seven of
eight one-line mutations of the fixes failed the module at once; the eighth (a rerun after every session came
back by hand must still complete) now has its own test. handoff_text is unchanged: the retro dry run
reproduces receipts/retro-handoff.md byte for byte. validate rc 0; the registry changes only the five rows of
the edited files.

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

The CC adjudication of #1116 at 376f2bb (2026-10-14, bound to the CC micro
39cf4d73) ruled two of Astra's P2s P1: each could open a second live session
of one lane, and no lock or session-id record guarded either.

- F1: a rerun reset an unconfirmed or uncertain session to pending and opened
  it again, while the first `claude -n` could still register; a run stopped
  inside a call (TimeoutStartSec) left no record of it. Each call is now
  recorded in flight before it is made. A rerun leaves an unconfirmed,
  uncertain or in-flight session as it is; only the command center reopens
  one, by name, with `relaunch --reopen <name>`, recorded under `reopened`.
- F2: a failed pre-open query on the first attempt went on to open a tab. A
  call is now made only when the live list reads the session as absent; an
  unreadable list skips the session (`skipped`, nothing opened, retried by a
  rerun).
- F3: plan() gave two roster rows of one name two turns. One turn per name;
  the second row is left to the command center in the handoff.
- F4: the journal-missing and unreadable-state skips exited 0. Both are
  refusals now (exit 1), and the relaunch unit gains
  OnFailure=stack-alert@%n.service, the handler mineru.service uses
  (systemd.unit(5), OnFailure=).

The README documents the outcomes, the rerun rule and the reopen command; the
evidence README and the decision state 45 tests.

Tests: 45, all OK. Against 376f2bb's module, README and unit the seven new
tests fail (8 failures and 1 error; F1 fails in both its subtests). Two tests
that stopped a run at the tab call now stop it at the live-list read before
the call, since a call stopped in flight is unresolved by design and has its
own test. Each of the seven fixes, reverted alone, fails the module. The retro
dry run still reproduces receipts/retro-handoff.md byte for byte.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 15, 2026
The CC adjudication addendum of #1116 at 307f686 (2026-10-15, bound to the
CC DELTA micro c034ba4d) found two P1 gaps in item 2 of the blocker commit,
each able to open a second live session of one lane, and allowed one commit
scoped to that invariant.

- P1-a: a call's result was saved with the outcome still pending, so a run
  stopped in its settle wait left a lane a rerun opened again. The result is
  now saved together with the provisional outcome (unconfirmed after rc 0,
  uncertain after a timeout) before the wait. A session is unresolved when
  a call since its last reopen opened, timed out or is in flight, whatever
  its outcome label says.
- P1-b: a direct --reopen run could overlap the unit's run, and both
  rewrote the whole state. Every relaunch run holds an exclusive fcntl.flock
  on relaunch.lock in the state directory; a run that finds it held is
  refused, exit 1, writing and opening nothing.
- A reopen is spent only by the call it makes (resolved_through), so a
  --reopen run that is skipped or stops before its call leaves the lane
  unresolved.
- A --reopen name that is not an unresolved session this relaunch still has
  to bring back is refused, exit 1, with a refusals entry in the state file.

Tests: 49, all OK. With 307f686's module, the four new tests fail (6
failures across their subtests, plus the count check); the test file is
byte-identical at that run and at this commit (sha256 a0d213c6...). Each of
the five fixes, reverted alone, fails the module. The retro dry run still
reproduces receipts/retro-handoff.md byte for byte. The README and the
decision describe the lock, the provisional outcome and the reopen rules and
state 49 tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…imer and a boot oneshot on the systemd user manager

WSL stopped at 06:41:17Z on 2026-10-14 and no session came back when the distro started at 12:48:57Z. A
systemd --user timer now writes the live sessions (claude agents --json) to a per-boot roster each minute; a
boot oneshot reads the newest earlier roster and, unless that boot ended in a planned restart, writes one
sealed handoff and opens a fresh tab per profiled session, command center first, through the command
center's open-tab.ps1 and launch.sh. Native background sessions restart after a shutdown only on attach or
reply (agent-view docs L884-886, L982) and the 2.1.299 supervisor's service install is disabled.

Measured inside a running boot: interop from a user unit (rc 0, Windows session 1) and a tab that ran back
in the distro; 22 unit tests, one of them a structural check that no published sentence of this change
attributes a quoted phrase to the owner; a retro dry run on the real journal boots (simulation). The
registry adds only this change's 15 rows. Nothing is installed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…d journal refusals, no repeat of a timed-out tab, private modes

The first read of #1116 at 7a50475 asked for four P2 fixes; the local review found two of them and one more.

- A failed `claude agents --json` is no longer an empty roster. list_sessions returns None; snapshot keeps the
  last good roster and exits 1; relaunch asks again for up to client_wait_s, then refuses, writing and opening
  nothing.
- relaunch-<boot>.json replaces the relaunched marker: status in_progress, partial or complete, each run, and
  each session's calls and outcome (pending, running, failed, uncertain, unconfirmed). Only complete stops a
  relaunch. A rerun reuses the sealed handoff (checked by sha256) and opens only the sessions not running. A run
  first writes this boot's roster as the live sessions plus the ended boot's rows not running yet, and the
  minute snapshot carries those rows while the run is in progress, so a crash before the timer's first
  snapshot is recovered by the next boot. The cap counts runs.
- A timed-out tab call (tab_timeout_s) is an uncertain side effect: the run waits settle_s for the session and
  never calls again. Only a definite failure is retried, after the live list shows the session still absent.
- Rosters, the state and the planned marker are 0600, the handoff 0400, the state directory and rosters 0700;
  the handoff directory is made 0700 only when the tool creates it.
- No journal entry for the ended boot is a refusal: the roster's age at that boot's end cannot be checked.
- The README's plan-restart command runs the installed tool through /usr/bin/python3 -I.

Tests: 38, 16 of them new and 6 changed. Against 9f0127c's module, README and config, 20 fail. Seven of
eight one-line mutations of the fixes failed the module at once; the eighth (a rerun after every session came
back by hand must still complete) now has its own test. handoff_text is unchanged: the retro dry run
reproduces receipts/retro-handoff.md byte for byte. validate rc 0; the registry changes only the five rows of
the edited files.

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

The CC adjudication of #1116 at 376f2bb (2026-10-14, bound to the CC micro
39cf4d73) ruled two of Astra's P2s P1: each could open a second live session
of one lane, and no lock or session-id record guarded either.

- F1: a rerun reset an unconfirmed or uncertain session to pending and opened
  it again, while the first `claude -n` could still register; a run stopped
  inside a call (TimeoutStartSec) left no record of it. Each call is now
  recorded in flight before it is made. A rerun leaves an unconfirmed,
  uncertain or in-flight session as it is; only the command center reopens
  one, by name, with `relaunch --reopen <name>`, recorded under `reopened`.
- F2: a failed pre-open query on the first attempt went on to open a tab. A
  call is now made only when the live list reads the session as absent; an
  unreadable list skips the session (`skipped`, nothing opened, retried by a
  rerun).
- F3: plan() gave two roster rows of one name two turns. One turn per name;
  the second row is left to the command center in the handoff.
- F4: the journal-missing and unreadable-state skips exited 0. Both are
  refusals now (exit 1), and the relaunch unit gains
  OnFailure=stack-alert@%n.service, the handler mineru.service uses
  (systemd.unit(5), OnFailure=).

The README documents the outcomes, the rerun rule and the reopen command; the
evidence README and the decision state 45 tests.

Tests: 45, all OK. Against 376f2bb's module, README and unit the seven new
tests fail (8 failures and 1 error; F1 fails in both its subtests). Two tests
that stopped a run at the tab call now stop it at the live-list read before
the call, since a call stopped in flight is unresolved by design and has its
own test. Each of the seven fixes, reverted alone, fails the module. The retro
dry run still reproduces receipts/retro-handoff.md byte for byte.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The CC adjudication addendum of #1116 at 307f686 (2026-10-15, bound to the
CC DELTA micro c034ba4d) found two P1 gaps in item 2 of the blocker commit,
each able to open a second live session of one lane, and allowed one commit
scoped to that invariant.

- P1-a: a call's result was saved with the outcome still pending, so a run
  stopped in its settle wait left a lane a rerun opened again. The result is
  now saved together with the provisional outcome (unconfirmed after rc 0,
  uncertain after a timeout) before the wait. A session is unresolved when
  a call since its last reopen opened, timed out or is in flight, whatever
  its outcome label says.
- P1-b: a direct --reopen run could overlap the unit's run, and both
  rewrote the whole state. Every relaunch run holds an exclusive fcntl.flock
  on relaunch.lock in the state directory; a run that finds it held is
  refused, exit 1, writing and opening nothing.
- A reopen is spent only by the call it makes (resolved_through), so a
  --reopen run that is skipped or stops before its call leaves the lane
  unresolved.
- A --reopen name that is not an unresolved session this relaunch still has
  to bring back is refused, exit 1, with a refusals entry in the state file.

Tests: 49, all OK. With 307f686's module, the four new tests fail (6
failures across their subtests, plus the count check); the test file is
byte-identical at that run and at this commit (sha256 a0d213c6...). Each of
the five fixes, reverted alone, fails the module. The retro dry run still
reproduces receipts/retro-handoff.md byte for byte. The README and the
decision describe the lock, the provisional outcome and the reopen rules and
state 49 tests.

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

The GPT read of 7aad5a0 (CC ruling 2026-10-15T01:50Z, one commit, no
loop) found one P2 and one P3 in the --reopen refusal path.

- P2: a --reopen refused because the relaunch is complete, or because
  max_relaunches is reached, returned before the only writer of the
  refusal record, so the record the README and the PR body promise was
  missing. record_refusal() now appends it to that boot's relaunch state
  under the run lock the run already holds. Runs, sessions and earlier
  refusals are kept, no run is added and no tab call is made. With no
  readable relaunch state for the boot there is nothing to add the entry
  to; the README now says so, and the refusal goes to stderr only.
- P3: the refusal write ran before the dry-run return. A dry run now
  records nothing.

Tests: 51, all OK. At the rebased 7aad5a0 (56f2fec) the new tests fail
(the complete and capped subtests, the dry run, and the count check). The
reader's eight regression cases (its test_review_paths.py, sha256
913b3afd...) pass, two of which failed before. The retro dry run still
reproduces receipts/retro-handoff.md byte for byte.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/cc-native-practice-session-relaunch-20261014 branch from 7aad5a0 to 6bd6d99 Compare October 15, 2026 02:22
@seathatflowsinourveins
seathatflowsinourveins merged commit c8d7fa9 into main Oct 15, 2026
32 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/cc-native-practice-session-relaunch-20261014 branch October 15, 2026 02:54
seathatflowsinourveins added a commit that referenced this pull request Oct 15, 2026
…hat registered and exited is absent (#1140)

#1116 follow-up (a), the local review's P2 at 7aad5a0 :533 (the final pass at
c8d7fa9 :547-553; review_output.json sha256 7e01f04a). A session seen running
earlier in a run and missing from the final live list kept its running outcome,
so the run said complete and plan() refused every later rerun as already
relaunched.

- The final pass marks such a session absent and moves its resolved_through
  past its calls: it was seen listed, so its tab start registered, and a rerun
  opens it again without a --reopen (unresolved() is unchanged for a session
  never seen listed).
- complete needs a final list that was read; an unreadable one leaves the run
  partial (exit 1, the alert), and a rerun reads it again.
- README: the absent outcome, the status sentence and the rerun list. The
  decision's test line and the evidence README count: 53.

Fail-first: both new tests failed on c8d7fa9 ('complete' != 'partial'); the
module runs 53 tests OK. Own logic; no upstream mechanism changes.

Co-authored-by: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 15, 2026
…n creates; cite the mechanism sources

The remaining P3s on tools/session-relaunch after #1116 (follow-up d). The
code of session_relaunch.py is unchanged.

- 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 and setting it to 0700). The README now says
  so; a dry run on a fresh state directory adds exactly relaunch.lock.
- 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.
- 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): cite the GPT read of 7aad5a0 by its sha256,
  97052bc9bfeee54b8dc2bbd33ff1a95a46b5f28632f7a8ff5926c4679dbdfc56, instead
  of two files that are not in the repository.
- 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), not the test module, manifests/evidence.json, PR bodies or commit
  messages; a test ties the stated count to the scanned list.

Tests: 56 (51 + 5), OK under python3 3.13.16 and /usr/bin/python3 3.14.4.
Each new test fails against the 0f4ee07 README, decision and evidence
README; the P3-3 comment fix is text-only. Six in-memory mutations of
session_relaunch.py (no file changed) each fail one of the two new
behavioural tests. validate.py passed after re-registering the four edited
files in manifests/evidence.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 15, 2026
…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 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