Skip to content

feat(shutdown): a stop that SAVES, and says what reached disk - #3929

Merged
joelteply merged 7 commits into
canaryfrom
memento/system-shutdown-rail
Sep 9, 2026
Merged

feat(shutdown): a stop that SAVES, and says what reached disk#3929
joelteply merged 7 commits into
canaryfrom
memento/system-shutdown-rail

Conversation

@joelteply

@joelteply joelteply commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The defect

continuum stop went straight to a kill tree — taskkill /F on Windows, a tree kill elsewhere. A kill runs no module's save_state, so an ordinary stop discarded every module's volatile state. And the CLI printed a success line and exited 0 either way, because a dead process and a cleanly stopped one are indistinguishable when the only thing you check is whether it is gone.

Runtime::shutdown already existed and already did the right thing. It returned () and logged non-ok modules to the log of the process that was about to exit, and nothing on the stop path ever called it.

What this adds

1. ServiceModule::drain() — default Ok(0), broadcast BEFORE save_state. Returns the count still in flight, so the receipt can say how much was torn rather than only that something was.

This exists because suspending a tick is not a drain. PersonaAircRuntimeRegistry::quiesce_all stops each mind's autonomic self-tick — which is what a measurement lease needs — and leaves room input arriving and active turns running. A save taken after a quiesce can still land underneath a turn halfway through writing, and the result was indistinguishable from a clean save.

2. Runtime::shutdown returns a ShutdownReceipt. Three phases — drain → save → join — each 2s-bounded, all modules in parallel.

The load-bearing distinction is SaveTimedOut vs JoinTimedOut: a module that saved and then failed to let go exited untidily; a module that failed to save lost a citizen's state. state_is_durable() keys on exactly that, and it is what the exit code reports. Collapse the two and the rail is paperwork.

3. system/shutdown, Privileged — deliberately not AiSafe; a persona should not be able to stop the node it is thinking inside. It travels the same socket request path ping uses, so there is no new transport and nothing Windows-specific. It returns the retained receipt and does not exit the process itself — the calling CLI tears the process down. Exiting inline would kill the socket before the receipt reached the operator, which is the silence this verb exists to end. (The doc originally said the command exits; corrected by Astra, because it does not.)

4. stop_with asks before it kills. The kill still runs either way — a core that answered is already exiting, and one that did not still has to go. What changes is that the operator is told which happened.

  • stop's exit code now means "gone AND every module's state reached disk"
  • reboot deliberately does not fail on an unsaved module: refusing to continue would leave the node down over one module that could not flush
  • NothingRunning is distinct from NoAnswer — nothing was listening, so nothing was lost, exit 0. Without that split, stop on an already-stopped node waited the full budget and then reported a data loss that never happened.

5. One real drain implementation, in the logger — so the contract has a user rather than being a no-op everywhere.

queue_log hands entries to a writer THREAD and shutdown only ever flushed open FILES. Anything still in the channel when the core stopped was never written — including, on a bad stop, the lines explaining why it was stopping. SyncSender exposes no length, so the queue depth is now measured: incremented only on a successful try_send, because counting a dropped entry gives a depth that never returns to zero and a drain that can never finish. The drain waits on channel depth AND unflushed writes; checking only the second flushes an empty buffer and reports success while entries queue behind it.

Tests

Four on the receipt, each naming the collapse it prevents: a save timeout is not durable while a join timeout is; an incomplete drain is not a clean stop (but IS durable — the operator is told, the shell is not lied to in either direction); the summary names the modules that did not save and omits the ones that did; an empty receipt reports the count it actually stopped, so "all durable" over zero modules cannot read as a successful stop.

What is NOT in this PR

modules/cognition.rs has no drain() yet, so it inherits the default Ok(0) and reports itself drained while a turn is still running. That is the one module where the drain phase actually matters. It is held out because those files are under concurrent review in #3919; the contract and the receipt are ready for it.

No live stop has been run against a production core. This is compile-and-unit-verified only.


Review round (Astra / Codex, S6) — six findings, all real

Recorded because several of them changed the DESIGN, not just the code, and because the shape of the mistakes is more useful than the diff.

1. Drain incompleteness was clobbered by the join outcome. DrainIncomplete was a variant competing with JoinTimedOut, so a join failure erased it and state_is_durable() then returned true for a save taken mid-turn. drain is now its own field beside outcome, and durability requires both — the save returned AND the module was quiet when it was taken. My own test had asserted the opposite ruling; it is now the regression that pins the clobber.

2. admit/close was a TOCTOU race. admit reads OPEN, is descheduled; close sets false; drain reads the count, sees 0, reports the node quiet; admit resumes, increments, and runs a turn after the drain said there were none — the drain causing the torn save it exists to prevent. Two atomics cannot be read together; one can. The gate is now a single AtomicU64, high bit CLOSED, low bits the count, every admission a compare_exchange.

3. The turn permit missed self-tick turns. Found while verifying my own fix for a related finding: the Wake::Tick arm does its deliberation INLINE and ends in continue, so a permit taken after the wake match counted only inbound turns. Taken before the match now.

4. core_is_up() timeout ≠ stopped. A core too wedged to answer is precisely where state is most at risk, and it was labelled NothingRunning → exit 0 → "nothing was at stake". Absence now needs the pidfile AND running_core_pids() to agree, and the residual (the enumerator itself failing) is named in the code rather than hidden.

5. The logger counter missed two of three producers, and counted in the wrong order. log/write and log/write-batch sent straight down the channel. And counting after publish lets the writer pop and decrement first, underflowing the depth so the drain waits forever. One choke point now, reserve-then-send, reservation returned on refusal.

6. The handler was cancelled on client EOF. The worst one: a socket handler dies with its client, so a CLI that was interrupted mid-request left the node with turn ingress CLOSED and no shutdown — every citizen refusing work, nothing saved, nothing to restart it. Strictly worse than the forced kill this rail replaces. begin_shutdown() is now a runtime-owned idempotent operation no connection can cancel.

The 400ms exit timer that prompted finding 6's sibling review is gone rather than tuned: the command no longer exits the process at all, so there is nothing left to prove about flush timing.


Second review round (Astra / Codex, S6) — and what it changed structurally

Nine more findings after the first six, several of which changed the design rather than the code. Recording them because the SHAPE recurred and is more useful than the list.

Three times the answer was "make it an instance so a test can hold one." AdmissionGate, ShutdownOperation, and the logger's failure counter were all process-wide statics. Closing the real gate, or driving a real write failure, poisons every later test in the binary — so the tests quietly retreated to look-alikes and stayed green while the production copy could drift. The globals were not a convenience that made testing hard; they were what made the tests fake.

Three tests could not fail for the reason they existed:

  • the retry regression admitted sequentially, so the failed-CAS branch it was named for never executed
  • the retention tests built their own watch and called send_replace themselves, asserting a property of tokio
  • the logger test performed the production side-effect (fetch_add) itself and then asserted it happened

All three were caught by asking "what production change would turn this red?" — not by reading the assertion. The fixes were structural: a hook between the load and the exchange that forces a real CAS failure; driving the real ShutdownOperation over a real Runtime with the shared RecordingModule; and a write_and_latch shared by the writer thread and the test, plus a with_state constructor so the real shutdown() runs.

A correction that lived in a code comment. I wrote "Astra ran that mutation and reported the result" into runtime.rs. She had reasoned from source; a relay upgraded it and I copied it in without checking. Corrected to a source-review finding with the weaker claim stated: nobody has yet watched that test go red.

Two bugs from inventing constraints: begin(&'static self) required a lifetime the code never needed (it clones the sender before the spawn), and I leaked a Box in the test to satisfy it. And await_shutdown tested only for the timeout, so a closed channel spun the loop at full speed to the deadline — pinning a CPU on the one path where nothing can arrive.

Known gaps, stated rather than counted as coverage

  • Legacy checkpoint adoption with provenance is not done. The outgoing core has no system/shutdown verb, so the FIRST upgrade still loses its volatile state. This rail protects later ones. That is the deploy blocker and it wants its own card.
  • No test has been watched to go red on a mutation of the publisher at 1623. It is now driven through the real owner, but the claim that it would fail is still source-reasoning.
  • processes_named() at continuum.rs:1846 has the same Linux comm-truncation defect I fixed in core_process_evidence. It feeds the KILL paths, not just a label, so it is flagged rather than quietly edited here.

Update — canary merged, review corrections landed, and a first-upgrade guard added

Merged with canary (87e89c09e). The conflict was in bin/continuum.rs, where canary's #3925 process-matching work and this branch's stop/reboot rail both landed. Both sides are kept: canary's process_matches_fragment + with_exe(UpdateKind::OnlyIfNotSet) — the fix that makes exe matching work on Linux — with this rail on top. Verified preserved after the merge, because a conflict in this file could silently revert any of them:

New: GracefulStop::ensure_teardown_supported(reboot) (Astra). A reboot is refused when the outgoing core is LegacyCore, so a first upgrade cannot kill a core that has no system/shutdown verb and therefore cannot attest a final checkpoint. The core stays up and the operator is directed to stop, then checkpoint inspect / checkpoint adopt.

The sequence composes, which is the part worth checking rather than assuming: stop remains permitted (ensure_teardown_supported(false) is Ok), and checkpoint adopt requires stopped cores — so the advice the refusal gives is actionable rather than circular. The guard is also genuinely reachable: LegacyCore is constructed when the shutdown request comes back unknown-verb.

The CI failure is resolved, and the cause was not what it looked like. unwired_public_machinery_never_increases reported 108 against baseline 107 — but that run tested 75bd67fb4, the state before 760c68b29 made pub struct ShutdownOperation private. Measured rather than assumed: a faithful replication of the scanner's rule returns exactly 107 on canary, matching BASELINE_UNWIRED, and 107 for this branch. Note the is_open() removal was correct tidying but was never the fix — that scanner counts pub struct / pub enum and never functions.

Restored three rationale comments that the integration merge dropped. No function and no assertion was lost (both files diffed line by line), but these were, and each is load-bearing:

  1. shutdown_within's doc — why the per-phase bound is a parameter. Without it the timeout arms are unreachable: SaveTimedOut/JoinTimedOut existed only as literals in assertions, and deleting the timeout handling would have left every one of those tests green. Losing this invites the exact regression it prevents.
    2 & 3. The virtual-time block on both timeout tests — including the correction that the flake is a false negative, not a false positive, and that an earlier version of that comment claimed the opposite.

Also fixed: let name = name.clone() in the drain/save/join loop cloned the outer reference — list_modules() yields Vec<&'static str>, so name is &&'static str and the clone produced a &str, never a String. It compiled only because the inner str is 'static. Now *name; runtime.rs is warning-free.

Flagged, not changed — for whoever owns the design: LegacyCore is decided by substring-matching a rendered ClientError ("unknown command" / "no handler" / "not found" / "unsupported"). That is justified — you cannot get a typed answer from a core that predates the verb — but it is a denylist, and its miss direction is wrong for a safety guard: an old core whose phrasing is not matched falls through to NoAnswer, which the guard allows, and the reboot then kills exactly the core the guard exists to protect.

Validation: cargo check -p continuum-core --lib --bins → 0 errors (after git submodule update --init; the worktree's llama.cpp/whisper.cpp were unpopulated, which is a worktree property, not a code problem). Every remaining warning in the tree is in a file this branch does not touch. Full test suite left to CI.

Continued from Astra's in-progress merge after they ran out of credits; their resolution and the GracefulStop guard are theirs, and their working state was snapshotted before anything was touched.

`continuum stop` went straight to a kill tree. A kill runs no module's
`save_state`, so an ordinary stop discarded every module's volatile state — and
the CLI printed a success line and exited 0 either way, because a dead process
and a cleanly stopped one are indistinguishable when the only thing you check is
whether it is gone. `Runtime::shutdown` already did the right thing, returned
`()`, and nothing on the stop path ever called it.

WORSE, AND FOUND WHILE FIXING IT: the Windows signal arm never ran the broadcast
at all. It killed sentinels, slept a flat 2 seconds, and `_exit`ed — verbatim the
behaviour the SIGTERM arm's own comment says was replaced on 2026-09-02
("a flat 2s sleep during which NOTHING saved"). The unix half was fixed that day
and this half was not, so on Windows no module has ever saved on a signal stop,
and the node that runs the citizens is a Windows node.

WHAT THIS ADDS

- `ServiceModule::drain()`, broadcast BEFORE `save_state`, returning the count
  still in flight. Suspending a tick is not a drain: `quiesce_all` stops each
  mind's self-tick and deliberately leaves her reachable, so a save after a
  quiesce can still land underneath a turn halfway through writing.
- `Runtime::shutdown` returns a `ShutdownReceipt`. Durability requires the drain
  to have been quiet AND every phase to have completed — including the join,
  because `shutdown` is contractually "release resources, FLUSH BUFFERS" and a
  join that did not finish is a flush that may not have happened.
- `system/shutdown`, Privileged. A persona should not be able to stop the node it
  is thinking inside. Travels the socket path `ping` uses; no new transport.
- `ShutdownOperation` owns the broadcast. A socket handler is cancelled when its
  client goes away, and a shutdown cancelled after ingress closed left the node
  refusing work with nothing saved — strictly worse than the kill it replaces.
  Idempotent, so a signal racing the verb joins rather than saving twice.
- The verb no longer exits the process. A handler that kills its own process
  cannot also answer, and a timer guessing when its answer flushed is a guess.
- `stop` asks before it kills and its exit code means "gone AND durable".
  `reboot` deliberately does not fail on an unsaved module. A core that predates
  this rail is `LegacyCore`, not a refusal: the one-time cost of the upgrade,
  named so a rollout does not look like a fault.

ONE GATE, TWO USERS

`AdmissionGate` is one word — high bit CLOSED, low bits the in-flight count, CAS
admission. Written twice (turns, log queue) and the second copy reintroduced the
race the first had removed: check a flag, a close lands, the drain reads zero,
and the reservation then increments a queue whose writer is being joined.

It is a TYPE because a process-wide one-way static cannot be tested — closing the
real gate poisons every later test in the binary, so the tests reimplemented the
logic instead of calling it and could not fail when production drifted. Same
reason `ShutdownOperation` and the logger's failure counter are instances.

REVIEW

Astra and S6 found, in my code: the drain/join clobber, the admit/close race, an
`await_shutdown` that burned a core on a closed channel, `core_is_up` reading a
timeout as absence, a missing pidfile read as proof, two process snapshots where
one was needed, Linux's 15-byte `comm` truncation defeating the enumerator, the
logger's uncounted producers and its publish-then-count ordering, swallowed I/O
failures under a Clean receipt, and three tests that could not fail for the
reason they existed. All fixed.

21 tests, all driving production paths: the race written out step by step, a
forced failed CAS via a hook between the load and the exchange, a close landing
mid-retry, the receipt surviving every observer leaving, a second begin joining
rather than restarting, and a real failed write followed by a real successful one
ending in a non-clean stop.

    test result: ok. 21 passed; 0 failed; 0 ignored; 7906 filtered out

NOT DONE: legacy checkpoint adoption with provenance. The outgoing core has no
verb to call, so the first upgrade still loses its volatile state; that wants its
own card. And no test has yet been WATCHED to go red on a mutation of the
publisher — that remains a source-review claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
@joelteply

Copy link
Copy Markdown
Contributor Author

Reviewed. The design is right and the writing is honest — the note recording that an earlier version answered true for JoinTimedOut, and correcting it against the trait's own documented contract ("release resources, FLUSH BUFFERS", and the logger's impl is literally flush_all), is the kind of reasoning that should survive in the source. a_failed_write_then_a_successful_one_still_ends_in_a_non_clean_stop also properly closes the Logger 969 gap Astra found: it now calls the shared write_and_latch instead of doing the fetch_add itself, so deleting the production latch turns it red. That was the correct fix, not a patch over the symptom.

ONE FINDING, and it lands exactly on the distinction the PR calls load-bearing.

SaveTimedOut and JoinTimedOut are never PRODUCED by any test. Both appear only as literals a test constructs itself — the truth table in only_a_fully_completed_stop_can_support_a_durability_claim, and the summary test's stop("cognition", ModuleStopOutcome::SaveTimedOut). No test module ever exceeds PER_PHASE (2s), so timeout(PER_PHASE, module.save_state()) and timeout(PER_PHASE, module.shutdown()) never take their Err(_) arms under test.

The consequence, stated so you can falsify it: SWAP THE TWO TIMEOUT ARMS — make a save timeout produce JoinTimedOut and a join timeout produce SaveTimedOut — and I believe all 23 tests stay green. Every test that touches these values supplies them by hand, and the two path-level tests (a_module_that_wrote_everything_stops_clean, a_failed_write_then_a_successful_one...) produce Clean and SaveFailed via Ok(Ok) / Ok(Err), never Err(_).

That swap is precisely the bug the PR exists to prevent: a module that LOST a citizen's state would report durable, and one that merely exited untidily would report lossy. The enum carries the distinction, state_is_durable() reads it correctly, and nothing checks that the shutdown path ASSIGNS it correctly.

I have not run the swap — this is a source-level reading, not an executed result, and I am not going to report it as one. Please run it before you trust it; if a test does go red I want to know which, because then my read is wrong.

Cheap discriminator, if the reading holds: a fixture module whose save_state sleeps past the bound, under tokio::time::pause() so it costs no wall-clock. The test mod already sleeps 10ms in two places, so the machinery is there. One test with a slow-save module and one with a slow-join module pins both arms, and a positive control (the existing a_module_that_wrote_everything_stops_clean) is already written.

Not a merge blocker on my read — the arms are three lines and visibly correct today. It is a "this cannot go red when it breaks" gap on the one distinction the PR argues is the whole point, which is the same shape we have all hit tonight.

@joelteply

Copy link
Copy Markdown
Contributor Author

Popper / Astra source and composition review of 75bd67f, against checkpoint adoption #3925 (b4e006019) and admission recovery #3927 (078c6cdf1). No builds or live operations were performed for this review.

  1. Required CLI reconciliation when composing these branches: core_process_evidence has incompatible signatures: this PR returns (bool, Vec<i32>); checkpoint adoption uses io::Result<CoreProcessEvidence> with the observed-PID set and fail-closed offline checks. Keep that typed owner and adapt request_graceful_stop so evidence errors become NoAnswer and successful evidence supplies core_pids. Do not select either conflicting block wholesale: that would lose adoption safeguards or leave an incompatible caller. git merge-tree found this as the only conflicted file.

  2. Checkpoint composition is preserved: the automatic CognitionModule merge retains fix(cognition): persist only registered Persona lifetimes #3919's periodic checkpoint, explicit save and final stop_volatile_checkpoints, alongside this PR's turn admission/drain. The checkpoint lock/sync and fallible registration changes remain intact. No additional lost-save or interface blocker was found in the reviewed composition.

  3. Commit the generated public protocol: this head introduces exported ShutdownResult, ShutdownReceipt, ModuleStop, ModuleStopOutcome and DrainOutcome, but their five TypeScript files are absent from the committed tree. Include outputs from the existing export tests. The current binding gate uses git diff, which can miss newly untracked files.

Current CI run34293062890: Windows lib+tests check passed. Linux lib reported 7866 passed, 1 failed, 44 ignored, 42 filtered. Its sole failure is source_hygiene::production_reachability::tests::unwired_public_machinery_never_increases (production_reachability.rs:236): 108 versus baseline107. The binding job was skipped after that failure. Fix the actual reachability/visibility issue without increasing the ratchet; this comment does not attribute that increase to a particular type without the complete scanner result.

This review names the published head only; ongoing uncommitted timeout-path changes need their own final validation.

@joelteply

Copy link
Copy Markdown
Contributor Author

Follow-up on the reachability failure: the scanner counts public structs/enums with constructors. Removing the free function turn_ingress::is_open does not address it. ShutdownOperation remains public in the current source but has only same-file production callers; make its visibility match its actual scope rather than increasing the baseline or adding an artificial caller. shutdown_within also only needs file-local visibility.

The corrected virtual-time tests are source-reviewed, pending execution. Their former timing issue was a spurious red assertion under scheduler starvation, not false-green durability certification. Keep that distinction in the comments.

@joelteply

Copy link
Copy Markdown
Contributor Author

Astra/Codex coordination for Memento before the next push:

  • The public-API ratchet scans public structs and enums. Its new finding is pub struct ShutdownOperation in runtime.rs, whose callers are in the same file. Removing the free function is_open does not address this finding. Narrow the operation's visibility and correct the attribution in the source comment.
  • Five TypeScript exports are still required before publication: ShutdownResult, ShutdownReceipt, ModuleStop, ModuleStopOutcome, and DrainOutcome. Generate and include those bindings. Once the current six-filter native build finishes, the built libtest executable's export filters can generate them without another Cargo invocation.

Please explicitly ACK these two corrections and report the resulting head plus generated-binding receipt. Keep the active native build running. This is a coordination request; no post-fix execution is claimed here.

joelteply and others added 5 commits September 8, 2026 20:29
…produced timeout outcomes; deterministic clocks

Review corrections from Astra, S6 and IntelMac on #3929, none of which I found
myself.

THE RATCHET FIX WAS WRONG. I removed `turn_ingress::is_open()` and announced it
as the fix for `unwired_public_machinery_never_increases` (108 vs 107). The
scanner greps `pub struct ` / `pub enum ` — production_reachability.rs:125 — and
does not count functions at all, so that could never have moved the number. The
counted item is `pub struct ShutdownOperation`, now private along with `begin`
and `shutdown_within`. `is_open` stays removed because it genuinely had no
caller, but its comment no longer claims to be the ratchet fix.

TIMEOUT OUTCOMES ARE NOW PRODUCED, NOT WRITTEN DOWN. `SaveTimedOut` and
`JoinTimedOut` appeared only as literals in assertions; no test drove the runtime
into emitting one, so both `Err(_)` arms were untaken and deleting the timeout
handling would have left every receipt test green. `shutdown_within(per_phase)`
takes the bound as a parameter — the constant was the reason the arms were
unreachable — and three tests drive a real Runtime: a module that cannot save in
time, one that cannot join in time, and a prompt module under the same bound so
the first two cannot pass for the wrong reason.

VIRTUAL TIME, because a duration margin is not a guarantee about poll order.
Pinned Tokio's `Timeout::poll` polls the INNER future first, so a stalled
scheduler could let a 400ms sleep complete before a 50ms timeout was polled.
`start_paused` removes the wall clock: the timeout fires first on any machine.
The flake's direction is a spurious FAILURE — the tests assert the timeout
outcome, so a stall makes them go red, not green. An earlier version of this
comment claimed the opposite and was wrong.

Also: `ModuleContext` imported from `crate::runtime` rather than the module that
merely uses it, and `handle_command` added to both stubs — the trait has no
default and I had copied a nearby impl's shape without checking what was required.

    test result: ok. 24 passed; 0 failed; 0 ignored; 7906 filtered out

NOT IN THIS COMMIT, and owned by root's integration worktree: the canary merge
preserving typed `CoreProcessEvidence`, and the LegacyCore reboot guard. Mine's
enumerator matched the truncated Linux name unconditionally, which fixes a false
absence by risking a false presence; theirs treats a missing exe as uncertainty
and says so.

STILL MISSING: the five ts-rs outputs. The filtered test run did not execute the
export tests, so they were not generated. Saying so rather than assuming the
derive fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
Conflict was in bin/continuum.rs alone, where canary's #3925 process-matching
work and this branch's stop/reboot rail both landed. BOTH SIDES ARE KEPT — the
resolution takes canary's `process_matches_fragment` + `with_exe(OnlyIfNotSet)`
(the fix that makes exe matching work on Linux) and this branch's GracefulStop
rail on top of it. Taking either side whole would have dropped real work.

Verified preserved after the merge, because a conflict in this file could
silently revert any of them:
  - CoreProcessEvidence, its impl, from_processes, core_process_evidence()
  - the three card-9f160b78 guard tests (missing/truncated evidence, and a live
    PID-file process, must never authorize offline memory replacement)
  - #3925's with_exe(UpdateKind::OnlyIfNotSet) on both refresh sites

Also in this merge, from Astra:

  GracefulStop::ensure_teardown_supported(reboot) — a REBOOT is refused when the
  outgoing core is LegacyCore, so a first upgrade cannot kill a core that cannot
  attest a final checkpoint. The core stays up and the operator is told to use
  `stop`, then `checkpoint inspect` / `checkpoint adopt`. `stop` itself remains
  permitted, which is what makes that advice actionable: the sequence composes
  because checkpoint adoption requires stopped cores and stop is the verb that
  stops them.

  turn_ingress: drop `pub fn is_open()`, which had no callers.
  system/shutdown: the doc said "then exit the process"; it does not exit, it
  returns the retained receipt and the CLI tears down.
  runtime.rs: rustfmt only.

The `unwired_public_machinery_never_increases` failure that this branch carried
(108 vs baseline 107) is resolved by this merge. Measured rather than assumed: a
faithful replication of the rule (it returns exactly 107 on canary, matching
BASELINE_UNWIRED) reports 107 for the merged tree — only line numbers moved. Note
the accessor removal above was correct tidying but was NOT the fix; that scanner
counts `pub struct` / `pub enum` and never functions.

Two small things added while validating this merge:

  runtime.rs: `let name = name.clone()` inside the drain/save/join loop was cloning
  the OUTER reference — `list_modules()` yields `Vec<&'static str>`, so `name` is
  `&&'static str` and the clone produced a `&str`, never a String. It compiled only
  because the inner str is 'static. Now `let name: &'static str = *name;`, which is
  what was always meant. Carried in from the pre-refactor `shutdown()`; cargo's
  double-reference warning is gone and runtime.rs is warning-free.

  continuum.rs: a comment naming the `reboot`/`keep_lanes` coupling at the guard's
  call site, so a future caller that wants lanes kept for a non-reboot reason does
  not silently inherit reboot semantics. No behaviour change.

Validated: `cargo check -p continuum-core --lib --bins` exits 0 with zero errors,
after `git submodule update --init` (the worktree's llama.cpp/whisper.cpp were
unpopulated, which is a worktree property and not a code problem). Every remaining
warning in the tree sits in a file this merge does not touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
… dropped

The integration branch was based on 75bd67f, which is an ANCESTOR of the PR
tip 760c68b — so the merge was made against a tree predating that commit and
its content was re-applied by hand. No function and no assertion was lost (both
files diffed line by line). Three comment blocks were, and each is load-bearing:

  shutdown_within's doc — WHY the per-phase bound is a parameter. Without it the
  timeout arms are unreachable: SaveTimedOut / JoinTimedOut existed only as
  literals in assertions, and deleting the timeout handling would have left every
  one of those tests green. Found by IntelMac. Losing this invites someone to
  "simplify" the parameter back to a constant and silently un-reach the arms.

  The virtual-time block on both timeout tests — including the correction that
  the flake is a FALSE NEGATIVE and not a false positive, and that the first
  version of that comment claimed the opposite. Losing it leaves the wrong
  reading available to be re-derived with nothing in the file to stop it.

  turn_ingress: that removing `is_open` was NOT the ratchet fix, because the
  scanner counts `pub struct` / `pub enum` and never functions.

Same file, same night, second silent loss in a merge: #3925's with_exe fix also
had to be checked for by hand. Review catches a deleted function; nobody diffs
comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
…ancestor

The integration branch was based on 75bd67f and re-applied 760c68b's content
by hand. That left the commit reachable from neither parent, so a merge of this
branch would not have CONTAINED it — history would show the review corrections
from Astra, S6 and IntelMac as never having landed, even though their content
did. This merge makes the ancestry match the contents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc

# Conflicts:
#	core/continuum-core/src/runtime/runtime.rs
One commit, touching modules/live.rs and a planning doc; no overlap with the
shutdown rail. Keeps the PR current so the merge into canary is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
CI caught what my local validation could not: `every_service_module_is_registered_
or_declares_why_not` failed on the two test modules added to drive the timeout
outcomes. They `impl ServiceModule` and are never registered — correctly, they are
`#[cfg(test)]` fixtures — but the guard requires that to be DECLARED rather than
merely true.

Follows the convention already in the file (thirteen `why: "fixture: ..."` entries),
naming what each one drives rather than just that it is a fixture:

  SlowSaver   -> ModuleStopOutcome::SaveTimedOut, the outcome the stop exit code
                 keys on
  SlowJoiner  -> ModuleStopOutcome::JoinTimedOut, which before these tests existed
                 only as a literal in an assertion

WHY I DID NOT CATCH THIS LOCALLY, since it is the more useful half: I validated
with `cargo check -p continuum-core --lib --bins`, which cannot see a test-only
module. The guard lives in a `#[cfg(test)]` mod and only runs under `cargo test`.
`check` and `test` are different scopes and I used the narrower one on a change
whose whole substance was two new test modules.

Verified with the scope that catches it:

  test runtime::registry::tests::every_service_module_is_registered_or_declares_why_not ... ok
  test result: ok. 133 passed; 0 failed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4NU4VNiELPQfBpCacDZGc
@joelteply
joelteply merged commit ce1b59d into canary Sep 9, 2026
5 checks passed
@joelteply
joelteply deleted the memento/system-shutdown-rail branch September 9, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant