Repository navigation
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 intoOct 15, 2026
Conversation
seathatflowsinourveins
marked this pull request as ready for review
October 14, 2026 13:22
5 tasks done
seathatflowsinourveins
force-pushed
the
claude/cc-native-practice-session-relaunch-20261014
branch
2 times, most recently
from
October 14, 2026 20:25
7125fff to
9f0127c
Compare
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
force-pushed
the
claude/cc-native-practice-session-relaunch-20261014
branch
from
October 15, 2026 02:22
7aad5a0 to
6bd6d99
Compare
seathatflowsinourveins
deleted the
claude/cc-native-practice-session-relaunch-20261014
branch
October 15, 2026 02:54
This was referenced Oct 15, 2026
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]>
5 tasks done
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope
systemd --usertimer writes the live sessions (claude agents --json) to a per-boot roster every minute.open-tab.ps1andlaunch.sh.relaunch-<ended boot>.jsonrecords 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.tools/session-relaunch/README.md, and installing is the command center's act.8c11dac090f71d340abd54abf8c67ffae13fa2d5lane:foundationtools/session-relaunch/:session_relaunch.py,README.md,config.example.json,units/tests/test_session_relaunch.pydocs/decisions/2026-10-14-session-relaunch-after-wsl-restart.mdevidence/artifacts/session-relaunch-20261014/manifests/evidence.jsonSOTA sources
The relaunch builds on the layer's recorded supervisor and the client's own interfaces; nothing new is installed.
catalogs/landscape/foundation.jsonline 3376,scheduling-supervision. Current choice: "Dagu for selected local workflows and systemd user-manager supervision". Winners: systemd255.4-1ubuntu8.17and dagu2.16.6. The host runs systemd 259.claude agents --help, lines 24-25: "--jsonPrint 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".c8e2b81092b94b4b3ded2c86b40b8329e73b8f39b36acb3288bd56385bc83266.7abf62a9…, line 394 (2.1.295):claude agentsstopped "(macOS, or Linux without the service installed): running sessions now stop in about a minute".docs/decisions/2026-09-28-terminal-experience.mdline 616. Windows Terminal'spersistedWindowLayoutstarts a fresh process from each tab's saved command line and reattaches nothing.claude-native-practiceskill's scheduled-tasks slot, which defaults to systemd--userunits. Its note: "after a shutdown nothing fires until the distro starts".launch.sh).Evidence-class table
systemd --userunit runspowershell.exe(rc 0, Windows session 1) withoutWSL_INTEROPnative_proven(inside a running boot)evidence/artifacts/session-relaunch-20261014/receipts/probes.jsoninterop-from-user-unitnative_proven(inside a running boot)tab-from-user-unit;probe/tab_probe.ps1,probe/tab_probe.shclaude agents --jsoncosts 0.18-0.23 s per callnative_provenagents-json-cost~/.claude/sessions/kept no file for any of the 7 sessions alive at the stop;pidDomainis the machine idnative_provensession-files-after-the-stopsource_reviewreceipts/sources.jsonsynthetic(simulation on the real journal boots)probe/retro_dry_run.py,receipts/retro-handoff.mdsynthetic(stand-in client and PowerShell)tests/test_session_relaunch.py(51 withPublicationSurfaces)local_integration(structural check; fails at the first read head7a504750)tests/test_session_relaunch.pyPublicationSurfacesrelaunch-<boot>.jsonrecords each call and outcomeLocal commands run
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, replacing7a504750): the first read's blocker 1full1-pr1116-7a5047505/GPT-VERDICT-7a5047505.md, sha25615703c8d.... It is cited by file and number; its shape stays in that file and is not restated.PublicationSurfaceschecks every published file of the change. It fails at7a504750.Fix round 2 (one commit,
376f2bb9, on9f0127c8): the first read's four P2s and the local reviewThe co-op's privacy scan of
9f0127c8was clean first. Against9f0127c8'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.list_sessionsreturns None instead of an empty list.snapshotthen keeps the last good roster and exits 1.relaunchasks again for up toclient_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.relaunch-<boot>.jsonreplaces therelaunchedmarker. It holds the status (in_progress,partialorcomplete), each run, and each session's calls and outcome (pending,running,failed,uncertainorunconfirmed).completestops 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.in_progress.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.tab_timeout_sis never repeated. The run waits for the session (settle_s;cc_wait_sfor the command center) and records it asrunningoruncertain. 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.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:194assertion now 0400),test_an_existing_handoff_directory_keeps_its_mode.test_without_the_ended_boot_in_the_journal_nothing_is_relaunched.plan-restartcommand runs the installed tool through/usr/bin/python3 -I. Test:test_the_readme_runs_every_installed_command_through_python.cmd_snapshot,planandhandoff_text, and the handoff text. The retro dry run reproducesreceipts/retro-handoff.mdbyte for byte.Fix round 3 (the blocker commit
307f686con376f2bb9): two paths to a second live sessionThe 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:unconfirmed,uncertainor in-flight session as it is, because its firstclaude -nmay still register. Only the command center reopens one, by name, withrelaunch --reopen <name>, recorded under the run'sreopened. Tests:test_a_rerun_does_not_reopen_a_tab_start_it_could_not_confirm(both outcomes) andtest_a_run_stopped_during_a_tab_call_leaves_it_unresolved.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.test_two_roster_rows_of_one_name_get_one_turn.OnFailure=stack-alert@%n.service, the handleradoption/templates/systemd/mineru.serviceuses (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.test_the_evidence_readme_states_the_module_test_count.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.receipts/retro-handoff.mdbyte for byte.recipes/mineru-local-service.mdon main already carries.Fix round 4 (one commit,
7aad5a02on307f686c): the item-2 invariantThe command center's DELTA read of
307f686cfound 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:pending, so a rerun opened the lane again. The result is now saved together with the provisional outcome (unconfirmedafter rc 0,uncertainafter 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.--reopenrun overlapping the unit's run: every relaunch run holds an exclusivefcntl.flockonrelaunch.lockin 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.--reopenrun 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.--reopenname: a name that is not an unresolved session this relaunch still has to bring back is refused, exit 1, with arefusalsentry 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).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 (sha256a0d213c6…); the receipt isp1b-fail-before.txt, sha256f8e00bb8…. Each of the five fixes, reverted alone, fails the module (p1b-mutants.txt, sha256f9eabc36…).receipts/retro-handoff.mdbyte for byte.Fix round 5 (registry-only rebase onto main, then one commit
6bd6d99d): the refusal recordThe rebase has no other content change.
7aad5a02was rebased onto main929bc3c3under the command center's standing rule:manifests/evidence.jsonwas the only conflict, the 15 branch rows were re-registered,lg_change_idis0c6c3bbe…before and after, merge-tree exits 0, and the rebased parent is56f2fec4.The GPT read of
7aad5a02found 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.--reopenwith no record: a--reopenrefused 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).test_a_dry_run_records_no_refusal.56f2fec4the new tests fail (p5-fail-before.txt, sha2561528fd11…). The reader's eight regression cases (itstest_review_paths.py, sha256913b3afd…) pass at the new head, two of which failed before.6bd6d99d, and so doesvalidate.py(12,040 files).Follow-ups
tools/session-relaunch/README.md, including the inverse.wsl.exe./usr/bin/python3 -I "$T/session_relaunch.py" plan-restart --config "$S/config.json"first, so only its own path relaunches.relaunch-<boot>.jsonfor each call and outcome, then record the boot-time measurement. Apartialrun makes the oneshot exit 1, so it shows as failed insystemctl --user --failed; the README has the rerun.376f2bb9): the partial carry (:221), a stalerunningoutcome (: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.7aad5a02(PASS; no double-launch sequence found), its P3s and open checks::456-459).--reopenstill writes arefusalsentry, because the reopen check (:462-471) runs before the dry-run return.:464), and a timed-out call that is saved asuncertain.powershell.exeoutlives its WSL interop stub.7aad5a02, in the command center's order::533: reconcile the sessions before marking the relaunch complete. A lane that registers and then dies is left dead and unrecovered.:496: a cross-process roster lock between the seed roster and the timer's snapshot.:222: carry a partial run's unfinished rows.claude --resumefor a second session of a name relaunched above.TimeoutStartSec=900against the retry and settle budget, and command-center readiness before the workers;AUTO-RESTART-*files, and the install assumes~/.config/systemd/user/;Checklist
🤖 Generated with Claude Code