Complete native owner/package cut-over rehearsal and operator manifest

Card: SAU-384
This commit is contained in:
SAU-384 implementer
2026-10-31 13:49:57 +00:00
parent 17d1c52eca
commit e4c8fcafbb
3 changed files with 104 additions and 20 deletions
+10 -9
View File
@@ -2,19 +2,20 @@
Authority: [/docs/design/plugin-handover/design.md](/docs/design/plugin-handover/design.md), Rollout; [/docs/symphony/artifacts/SAU-356/plan.md](/docs/symphony/artifacts/SAU-356/plan.md), Approval and operator gates.
**Status: blocked, not a live execution approval.** Rehearsal evidence and the reviewed target bundle are under [/docs/symphony/artifacts/SAU-384](/docs/symphony/artifacts/SAU-384). No shipped plugin or host behavior changes on this branch. The temporary migration implementation and native controller were executed only in a throwaway box and deleted after the successful migration verification. Their hashes/deletion record and source-as-log remain, not an executable migration tool.
**Status: rehearsal complete, ready for review; not a live execution approval.** Rehearsal evidence and the reviewed target bundle are under [/docs/symphony/artifacts/SAU-384](/docs/symphony/artifacts/SAU-384). No shipped plugin or host behavior changes on this branch. The temporary migration implementation and native controller were executed only in a throwaway box and deleted after the successful migration verification. Their hashes/deletion record and source-as-log remain, not an executable migration tool.
## Required resolution before completing the kit
## Closed integration gaps
1. Pin the actual external host launcher/service owner, stop/start interface, host architecture and runtime closure. Those are not projected into this world. The supplied local native artifact is **aarch64-linux**, dynamically linked to the Nix paths printed in the build evidence; it is NOT a verified live-host package. Do not guess a systemd unit, use `core.restart` as stop-all, or install a mismatching binary.
2. Resolved rollback choice: **unsubscribing works; merely re-keying the registration does not**. An actual pre-best-effort session component, reading a registration written by the target component, ignores `best_effort` and sends history to its new ID. Rehearsal: unsubscribe = one barrier callback and no gateway-shaped replay; re-key = barrier plus historical replay. The test used the actual old manager and a native callback sink, not browser SSE. The runbook now makes unsubscribe mandatory, using the card's authorized option. Do not claim an ID rename is safe.
3. **Closed in run 1369:** armed/exit-race delay-gate tests passed on real core with actual target WASM; replacement orchestrator readiness/one-owner/all-watch arming passed; actual old-manager/gateway rollback without re-registration reproduced unsubscribe safety and ID-only re-key replay. See [resume evidence](SAU-384-resume-1369.md). The delay/observation hooks were only in a disposable host copy, not the selected host artifact or committed code.
- **Run 1373:** after mediator installed the observed public host key, approved read-only SSH inspection identified the actual Herdr pane, outer `sausage-loop`, world path, native architecture, old package/derivation/source and runtime closure. The actual old Nix package/loop → stop-all → offline swap → exact new Nix release package/loop passed in the disposable live-size world. A non-parent external controller separately proved loop-first stop, interruption/resumption, stale-PID refusal and safe frozen rerun. A real core.restart after loop exit could not respawn the host. See [native evidence](resume-1373.md).
- **Run 1369:** armed/exit-race delay-gate tests passed on real core with actual target WASM; replacement orchestrator readiness/one-owner/all-watch arming passed. Hooks existed only in a disposable host copy, never the selected host artifact or committed code.
- **Rollback:** actual old-manager/gateway tests prove **unsubscribe works; ID-only re-key does not**. The old binary ignores best_effort and replays history to a renamed ID. The runbook mandates the card-authorized unsubscribe option and prevents web re-registration during a session revert. A physical browser/SSE display is not claimed.
- Selected coherent bundle is now **`cutover-targets.tar.gz`** with the Nix native package and complete closure manifest, reviewed plugins/web assets and SDK `src/lib.rs`; it excludes the diagnostic debug host. The older `target-artifacts.tar.gz` remains prior evidence. Executed one-off tool/controllers were again deleted after verified success; source survives as log only.
Use the card's explicit **unsubscribe** rollback option; an ID rename is not a safe alternative. The remaining blocker is the actual external host launcher/native installation manifest. Read-only SSH credential request **055995a51608c57dd25b323bea731fcb** is approved, but connection was refused before authentication for an unknown Citadel host. Mediator must add the observed public host key, then resume inspection; workers may not change live config. See [run 1372](SAU-384-resume-1372.md). No new approval request or live stop/restart permission is needed/granted.
The final frozen inventory/caller-schema/hash check remains an operator gate at the actual approved shutdown, not something a read-only rehearsal can perform in advance. Live execution still requires Surma's go and mediator/orchestrator action; no approval request is pending.
## What was built and exercised
- Host source `e09c4f9f0ef6b5833d6be066771eecd9e0a1f572`. Disposable Nix build, native workspace suite passed after correcting the cold SDK-probe dependency cache. Target native binary was built and stripped, not released/deployed. CLI `--version` passed. Native controller used the real `sausage::Host`, real VFS, watches, locks, service supervisor and WASM execution; model/skills/memory/ledger/push effects were synthetic or loopback only.
- Host source `e09c4f9f0ef6b5833d6be066771eecd9e0a1f572`. Original debug native controller exercised real `sausage::Host`, VFS, watches, locks, service supervisor and WASM with synthetic/loopback effects. Run 1373 additionally built/rehearsed the actual **Nix release wrapper/package** `/nix/store/spyd9rix6r38m6vxfppyy9z83yp5dh85-sausage-0.1.0`; its empty-caller-PATH runtime check built/tested a real WASI+C plugin. Workspace suite passed again; all 105 Rust source files matched the tested tree. Neither host was released/deployed or selected by the live launcher. Native artifact/closure hashes are in `evidence/native-host-manifest.json`.
- Reviewed session `cb844151` / SAU-409 final; orchestrator `c80a5854` / SAU-408; web `d883fbf2` / SAU-403; agent/board retained reviewed components. Fresh card-worktree builds: mail **1600 / 53 tests**, cred **1601 / 272**, cron **1602 / 24**, rooms **1603 / 59**, all passed. No activation. Component/source provenance and every bundled file hash are in `evidence/manifest.json`.
- Frontend is exactly approved commit `3d76026749a5d21722033bbfe8743ac368946b0f`, retained production build from SAU-375 rework-1318. It remains `publish:hold`; no publication was attempted.
- World SDK runtime sources already matched the selected host SDK. The only needed source-copy delta is **`src/lib.rs`**, documenting effective grace. Preserve world-owned Cargo manifests, relocated `interface`, and imports. A first broad SDK-copy attempt incorrectly included repository-relative Cargo paths; comparison caught this before any live change, and the final migration used only the one-file patch. Never copy the repository's SDK Cargo manifests over the world SDK.
@@ -44,7 +45,7 @@ The clean final live-size end-to-end gate measured manager acknowledgement **0.6
## Resume verification
Run 1369 forced recompilation and repeated the complete workspace suite on the restored original host source: pass, including **208 host integration tests**. All **144 source files** matched the selected source snapshot. Full log `/logs/box/2026-10-31/tc-asa3yr-exec-3838117964.stderr` (host tests lines 64–434; equivalence line 785). New tests and their limitations are detailed in `SAU-384-resume-1369.md`; new test controllers and instrumentation were deleted after successful checks. Original bundle and production source are unchanged.
Run 1369 forced recompilation and repeated the complete workspace suite on the restored original host source: pass, including **208 host integration tests**. All **144 source files** matched the selected source snapshot. Full log `/logs/box/2026-10-31/tc-asa3yr-exec-3838117964.stderr` (host tests lines 64–434; equivalence line 785). New tests and their limitations are detailed in `resume-1369.md`; new test controllers and instrumentation were deleted after successful checks. Original bundle and production source are unchanged.
## Evidence and failures
@@ -60,4 +61,4 @@ All following logs are under `/logs/box/2026-10-31/`:
The initial broad-copy fixture and earlier failed tests are not the final kit. `evidence/migration-plan.json` identifies the final verified migration. Provider billing/cancellation, power loss, hung host filesystem behavior, physical browsers and independent per-plugin recovery follow-ups are not claimed complete. No live DB was opened; the accepted session snapshot was reused. No live config/SDK change, stop, activation, release, restart, push, deployment or `/www` write occurred.
See [operator runbook](SAU-384-runbook.md) for approval gates and the deliberately blocked live steps.
See [operator runbook](runbook.md) for exact owner/package/selector steps, frozen-audit and approval gates. [Run 1373](resume-1373.md) records the final native rehearsal and remaining unverified live-only boundaries.
+74
View File
@@ -0,0 +1,74 @@
# SAU-384 resume — run 1373
**Ready for review, not live execution.** Authority remains the card/design Rollout and SAU-356 approval gates. No host/session/cred operation, push or web publication was authorized or performed. No shipped host/plugin behavior changed.
## Actual owner and native package resolved
After mediator installed the observed host key, reused the approved SSH grant for **read-only** inspection as `surma`. The actual owner is Herdr workspace **w7**, tab **w7:t5**, pane **w7:p6**. Its shell runs `/nix/store/pz2b94a2v99r3msljdz82h7n3i81hhh5-sausage-loop/bin/sausage-loop /home/surma/world/`. This is not a systemd Sausage unit. The loop uses a mutable remote flake, exits on zero and refreshes/restarts after any nonzero exit (its existing sleep is 2 s). Therefore stopping plugins or requesting core.restart is not a deliberate native stop boundary.
Citadel is **aarch64-linux**. Observed old package, derivation, original source and wrapper/binary hashes are pinned in `evidence/resume-1373-inspection.json`. Read the old derivation's actual CLI source: SIGTERM awaits `Host.stop`; an already-requested restart can still return 75. Do not reuse observed PIDs as constants.
Built the exact Nix **release** package from the unchanged card host source:
- `/nix/store/spyd9rix6r38m6vxfppyy9z83yp5dh85-sausage-0.1.0`
- `bin/.sausage-wrapped` SHA-256 `683623c4c77dbcfff590c9f0aa78f045d07935a466836a099ccc53d7336853cb`
- Package NAR and every closure path/hash: `evidence/native-host-manifest.json`. ELF is AArch64 with the pinned Nix glibc interpreter. The closure equals the observed old package's closure except for the host root itself.
- Empty caller PATH runtime check **passed**, building/testing an actual WASI plugin with C and the packaged toolchain; it was rebuilt with visible output, not inferred from `--version`.
The box shares Citadel's Nix store: this build made an inert store path visible there, **not** selected by the live launcher. No live package/configuration/boot/SDK was changed.
## Native end-to-end and interruption tests
The disposable live-size world reused the accepted SAU-362 snapshot (1,491 sessions / 580,355,405 bytes). Executed the actual old Nix package and inspected loop, actual old mail/session components and actual target gateway; then:
1. Recorded old plugin ownership through the real gateway/proc projection.
2. External controller terminated the **outer loop first**, awaited its pidfd exit, then SIGTERM'd the exact old host and awaited its exit. The host survived its parent until its own graceful shutdown; controller survived outside the group. One final group inventory was empty; no process polling.
3. Backed up frozen state, applied the approved queue/mail losses and cron conversion, injected interruption after operation 3, completed and repeated the hash-guarded offline plan.
4. Changed a static external selector from the old to the exact new package. Marked started before launch. Started the same actual loop with explicit `SAUSAGE_FLAKE=path:<selector>`.
5. Verified the running OS executable was the new Nix binary; exactly one target manager, no old mail deliverer, locked cred, real subscriber-registration completion. After-start apply/whole-world rollback refused. Marked verified, then stopped cleanly.
Separate **non-parent controller** test used the reusable one-off `native_freeze.py` positional interface, not Python's parent-only wait(): pidfds pin the old loop/host, boot ID/start ticks/executable/group guard identities, owner exit precedes host stop. It proved interruption after owner exit then resume, stale-PID refusal without stopping the host, successful freeze and harmless frozen rerun. Another real-core test requested **core.restart after loop exit**: host exited and no owner/process in the old group could restart it. These tests close the external-owner gap; earlier run 1369 remains the evidence for armed/exit races, all-watch orchestrator upgrade ordering, and actual gateway rollback.
Native gate inventory contained 4,434 files. Of the 4,372 non-operation files, 4,370 stayed byte-identical after target activity; the only changes were the expected manager registration file and core journal. Histories/settings/mail/jobs/grants outside intended operations remained equal. Prior offline tests additionally proved exact losses, unknown mail retention, SQL ID/calendar/outcome preservation, grant preservation and rollback before start.
## Caller-store observation
Read-only SQLite connections (`mode=ro`, query_only, read transactions) inspected board, cron and rooms; JSON workflow/config stores were also inventoried without printing prompts or grants. See `evidence/resume-1373-inspection.json`:
- Six legacy cron session templates need the already-tested one-time conversion. Do not execute historical jobs or rewrite RPC words inside their prompts.
- Both board's mediator hook **and invariant target** are already target-valid `{session,text}` calls. Preserve their bytes.
- Eight historical running board session-attempt snapshots are diagnostic/uncertain, **never replayed** (`hooks.rs` explicitly documents this). Zero board subscriptions/batches were observed.
- One abandoned room has no pending turn/review/split. Orchestrator run/skill-review records have no embedded old session/agent call objects; zero host jobs and no live session-manager registrations were observed. Preserve typed workflow records.
- Three board runs had no outcome at observation, including this worker. This is old in-flight work, not proof of settlement or permission to replay it.
The operator must repeat this audit on the **frozen** backup at the approved shutdown. This changing live observation cannot be that future snapshot. Unknown executable references block preflight; no shipped compatibility path was added.
## Selected review artifacts and live steps
Selected coherent bundle: **`/docs/symphony/artifacts/SAU-384/cutover-targets.tar.gz`** (24,591,858 bytes), SHA-256 `1bbcae4ef615ca8394b0c53b9a408441a49f8a618f059335c3bc248d958977c4`. The 22 payloads are hash-pinned in `evidence/manifest.json`; bundle also carries that manifest. It contains the native NAR/closure manifest, reviewed plugins, approved web `3d760267`, and SDK **`src/lib.rs` only**. It excludes the earlier diagnostic debug host. Older `target-artifacts.tar.gz` remains immutable prior evidence, not the selected native install.
[Runbook](runbook.md) now gives actual owner/world/package paths, exact loop-first stop/checkpoint interface, proposed external root `/home/surma/sau-384-cutover` (absent at inspection), pinned selector and restart command, five-entry boot order (session before web; no mail.start), frozen caller conversion, web-release/rollback boundaries and mandatory executable deletion. Surma's go precedes mediator's live core/session/cred operation; every push separately needs go. Only mediator/orchestrator lands/executes; SAU-375 stays publish:hold until session and web are active, then orchestrator.release.
All five executed one-off/migration/test scripts were **deleted after verified success**; Python bytecode cache was empty. Hashes/deletion proof: `evidence/native-1373/native-tool-deletion.json`. Source is retained **as log only**, never as a shipped plugin or retained migration executable. The live mediator must reconstruct its reviewed temporary copy, execute only after go, and delete it plus executable caches after verification too. Static target selector configuration must remain for the pinned native loop; it is not a migration tool.
## Exact checks/logs
All full logs below are `/logs/box/2026-10-31/`:
- Nix package build/CLI: `tc-asa3yr-exec-1149169379.stderr`; packaged empty-PATH WASI+C gate: `tc-asa3yr-exec-395020656.stderr`.
- Actual native old stop/offline interruption/swap/new boot: `tc-asa3yr-exec-3989255528.stderr`.
- Non-parent controller interruption/stale-PID/frozen-rerun: `tc-asa3yr-exec-4245550011.stderr`; real core.restart after owner death: `tc-asa3yr-exec-2683606349.stderr`.
- Native architecture/closure equivalence/hashes: `tc-asa3yr-exec-1926991205.stderr`.
- Repeated `cargo test --workspace`: **pass**, including 208 host integration tests; `tc-asa3yr-exec-844023236.stderr`. All 105 Rust sources matched package/workspace copies. Run 1369's forced-recompile/source equivalence remains the earlier independent gate.
- Executed source-as-log and deletion: `tc-asa3yr-exec-2945036769.stderr`; 22 payload hashes/controller absence: `tc-asa3yr-exec-643070322.stderr`.
- New inventories/plan/losses/target process records/closure/source equivalence: `evidence/native-1373/`.
Prior builds remain mail **1600**, cred **1601**, cron **1602**, rooms **1603**; no plugin source changed. Their remote-less repos have no origin (fetch correctly rejected); merging shared local main was already-current for all four. Host fetch/merge was already current before work; final commit/fetch/merge is reported separately.
Failed approaches were corrected, not blockers: one incorrectly transcribed grant was rejected before any SSH command; the exact attached grant succeeded. zsh wildcard expansion and missing PATH tools were replaced by explicit installed Nix paths. `core.ps` does not exist; the native gate switched to the actual gateway `/fs/proc` projection and passed. Nix store logs were unavailable, so rebuilt the runtime check with visible `-L` output. One evidence-copy Path/string expression failed; corrected and resumed without re-executing migration. Full failure logs remain under /logs.
## Limits, not silently claimed complete
No new production-latency guarantee is claimed. Prior post-record real-core live-size samples remain: final manager acknowledgement **0.6078 s**, registration catch-up **0.0128 s**, mail.send-to-default publication **0.5376 s** (integrated **0.5235 s**), below callers' existing **3 s / 10 s** budgets. The integrated catch-up outlier **12.0419 s** remains disclosed. Orchestrator idle whole-host CPU was ~**0.10%** in the paused fixture; prior real cron warning/stop took **305.427 s**, and detached error data passed. These measurements and their synthetic/warm-cache limits are in implementation.md.
Live shutdown/frozen backup/push/activation/release/publication, physical browser display, power loss and provider cancellation/billing remain unverified/out of scope. Old unrecorded runs are not automatically inferred complete/resumed. Independent per-plugin recovery follow-ups remain independent; the design accepts first-update cuts until they land.
+20 -11
View File
@@ -1,16 +1,18 @@
# One-off handover operator runbook — blocked draft
# One-off handover operator runbook — rehearsed, approval required
Read [rehearsal/results](SAU-384-rehearsal.md), [resume verification](SAU-384-resume-1369.md) and [inspection prerequisite](SAU-384-resume-1372.md), the [authority](/docs/design/plugin-handover/design.md) and [approval gates](/docs/symphony/artifacts/SAU-356/plan.md). This is **not** authorization to execute. Workers must not perform these live steps. Mediator/orchestrator alone lands and executes.
Read [rehearsal/results](implementation.md), [race/rollback verification](resume-1369.md), [native owner/package rehearsal](resume-1373.md), the [authority](/docs/design/plugin-handover/design.md) and [approval gates](/docs/symphony/artifacts/SAU-356/plan.md). This is **not** authorization to execute. Workers must not perform these live steps. Mediator/orchestrator alone lands and executes.
## 0. Finish the installation manifest (no live mutation)
## 0. Verify and freeze the installation manifest (read-only first)
Evidence root: `/docs/symphony/artifacts/SAU-384/evidence/`.
Bundle: `/docs/symphony/artifacts/SAU-384/target-artifacts.tar.gz`.
Selected coherent bundle: `/docs/symphony/artifacts/SAU-384/cutover-targets.tar.gz`.
The older `target-artifacts.tar.gz` remains prior rehearsal evidence, **not** the native installation selection.
- Verify bundle hash against `bundle-sha256.txt`, then every extracted target file against `manifest.json`. This bundle contains target binaries/static assets and the one-file SDK patch, **not** a migration executable.
- Record the actual native service owner/launcher, its synchronous stop/start commands, automatic host restart mechanism, runtime closure and native architecture. Manifest fields are deliberately null until inspected. The bundled host is a local **aarch64-linux debug-profile, stripped** build of `e09c4f9f`; it has not been established as the right live-host installation package. Stop here on a mismatch; build/rehearse the right artifact rather than substituting an untested binary.
- Verify the new bundle hash against `evidence/cutover-targets-sha256.txt`, then every extracted file against `evidence/manifest.json`. It contains reviewed plugins/static assets, SDK `src/lib.rs`, and `native-package.nar.gz` with its closure manifest, **not** a migration executable. It excludes the old diagnostic debug host.
- Native selection is the Nix release package `/nix/store/spyd9rix6r38m6vxfppyy9z83yp5dh85-sausage-0.1.0`, aarch64-linux, built from this card's tree with unchanged host source `e09c4f9f`. `evidence/native-host-manifest.json` pins its NAR/file hashes and every runtime closure path/hash. All dependency paths match the observed old package's closure; only the host root differs. Recheck closure availability/hashes before execution. Mediator may import the exact reviewed NAR if needed (`gzip -dc native-package.nar.gz | nix-store --import`); never substitute the debug binary. Import/deployment is not a worker action.
- Owner is **Herdr workspace w7, pane w7:p6 (tab w7:t5)** running `/nix/store/pz2b94a2v99r3msljdz82h7n3i81hhh5-sausage-loop/bin/sausage-loop /home/surma/world/`, not a systemd Sausage unit. Re-identify its current loop/host PIDs using read-only `herdr pane process-info --pane w7:p6`; observed PIDs in `resume-1373-inspection.json` are evidence, never reusable constants. The loop refreshes a remote flake after any nonzero exit, including core.restart (75). Its existing 2-second restart sleep is not a migration quiescence mechanism.
- Pin all old plugin hashes, full boot bytes, current SDK source hashes, caller configuration, and `/proc/*/info.json` inventory. Record active agent/tool processes and any external effects whose result is uncertain. Do not infer settlement from the last assistant text.
- Inventory executable caller payloads in cron jobs, boot and other saved callback/action/configuration stores using their predecessor schemas. History text mentioning an old RPC is not an executable payload. The tested converter covers `jobs.data` where `method == session.send`; it does not claim a full live caller-store audit. Run 1372 observed six historical legacy session cron jobs (all jobs have `next_run:null`) and one already-target-valid board session hook. See `evidence/resume-1372-caller-inventory.json`; the frozen audit must also cover board `invariant_target.data`, hook attempts/subscription batches, room/orchestrator workflow records and any session event registrations. Diagnostic call snapshots and RPC names inside prompt strings must not be rewritten/replayed as executable configuration. Unknown executable references, `session.work`, `session.cancel`, ambiguous `interrupt:true`, invalid text or unexplained notify destinations stop preflight for explicit resolution.
- Inventory executable caller payloads using predecessor schemas, not substring rewriting. Run 1373 read-only audit covered cron jobs; board hooks, invariant target, attempt snapshots, subscriptions/batches; room/workflow events; orchestrator runs, skill reviews and host jobs; manager registrations and configuration. `evidence/resume-1373-inspection.json` records six legacy cron templates requiring conversion, an already-target-valid board hook **and invariant target**, eight historical running hook snapshots that must NOT replay, zero subscription batches, one abandoned room with no pending work, and no live session manager registrations. The only observed conversion beyond boot is cron `jobs.data` where `method == session.send`. Repeat against the frozen bytes: this observation is not a transactional final snapshot. Prompt strings/diagnostics are not executable templates; preserve them verbatim. Unknown executable references, `session.work`, `session.cancel`, ambiguous `interrupt:true`, invalid text or unexplained notify destinations stop preflight for explicit resolution.
- Source references for predecessor formats: session `f9122585`, `src/main.rs` Paths (`pending.jsonl`, `cancel`) and `src/queue.rs`; cron `ba75c102`; target cron README's one-off conversion procedure. Active old component hashes are separately captured; do not equate a source reference with its compiled active binary without checking.
- Confirm the frontend artifact is approved commit `3d76026749a5d21722033bbfe8743ac368946b0f`; keep `publish:hold` until step 5. Do not publish a worktree snapshot or release it early.
- Rollback uses the card's explicit **unsubscribe** option. ID-only re-key is **unsafe**, proven again with the actual old manager and gateway in run 1369. Prevent the gateway from re-registering `web-chat` against an old session binary; never call `web.start` in that state. A different proposed “re-key” needs its own defined semantics/rehearsal.
@@ -21,9 +23,9 @@ Surma's go must precede the live core/session/cred operation. Every push require
Run the one-off controller **outside the world and outside the service/process group being stopped**. A plugin or agent in this world cannot be the controller: stop-all must stop it too. Recreate the narrowly reviewed temporary executable only for this approved operation, from the logged implementation with the resolved manifest; never add compatibility/transition code to a plugin.
Close the actual ingress and stop the **external host/service owner** synchronously using the pinned command. Await its shutdown/process-exit completion, not a loop over `/proc`, endpoint retry, sleep, or a set of stop requests. If an outer launcher would respawn the host, deliberately stop that owner too; do not edit boot config while agents can still run. `core.restart`/exit-75 is a restart request, not stop-all. The real core's `Host.stop` suppresses service restart admission and stops all trees in parallel. Design budgets are grace 5 seconds and separate EOF-stop 5 seconds, not arbitrary new timeouts.
Close ingress before stopping ownership. The tested exact native boundary is the deleted one-off `native_freeze.py` (source-as-log linked in resume-1373): invoke its reconstructed, reviewed copy **outside the world/group**, with current loop PID, current host PID and an external checkpoint path. Use external root `/home/surma/sau-384-cutover`, with the reconstructed scripts in `tool/` and checkpoint `evidence/freeze.json`. The observed Python interpreter is `/nix/store/33i3kijbra0cffncn3rmi9bdrav7a3y3-python3-3.13.15-env/bin/python3`. After Surma's go and fresh PID/identity inventory, the exact stop invocation is `PYTHONDONTWRITEBYTECODE=1 <that-python> tool/native_freeze.py "$LOOP_PID" "$HOST_PID" evidence/freeze.json` from the external root. Do not pass the rehearsal-only failure-injection argument. It pins boot ID/start ticks/executable/group and pidfds, sends SIGTERM to the **loop first**, awaits loop exit, checkpoints `owner_stopped`, then sends SIGTERM to the exact host and awaits its exit. Host SIGTERM awaits real `Host.stop`; its old source was inspected at the actual Nix derivation. After both exits, inspect the saved group once; any unexpected survivor blocks without blindly killing it. No process polling, endpoint retry, sleep or service-stop-request counting. Do not close the Herdr server or unrelated panes.
The rehearsal's native external controller awaited `Host.stop`, asserted its process table was empty, then awaited host exit. The live equivalent needs the actual launcher identity from step 0. **That identity is currently missing; do not invent a unit name.** Source config has `services.sausage.enable = false` and describes a Herdr panel; read-only systemd-web inspection corroborates a user Herdr socket, not a system `sausage.service`. Credential request `055995a51608c57dd25b323bea731fcb` is approved for read-only inspection, but SSH refused the unknown Citadel host before authentication. Mediator must install the observed public host-key line from `evidence/resume-1372-ssh-trust.json` in `/config/known_hosts` and resume inspection; worker configuration changes remain forbidden. No additional person approval is needed for that trust entry under the SSH skill. It is not permission to stop or change the launcher.
The non-parent-controller rehearsal proves loop exit does not kill the migration controller or old host prematurely; the host survives its parent and drains on its own signal. Interrupting after loop exit is resumable from the same pinned checkpoint; stale PID identities refuse. Even a real core.restart after loop exit cannot respawn the host. `core.restart` by itself is **not** stop-all. Core's shutdown suppresses service restart admission and stops all trees in parallel. Design budgets remain grace 5 seconds and separate EOF-stop 5 seconds; the controller awaits completion rather than inventing a live timeout. Never edit world state before the `frozen` checkpoint.
## 2. Back up frozen state and plan changes
@@ -35,7 +37,7 @@ Only after shutdown completion:
4. Convert each old cron `session.send` template once: require a JSON object/nonempty text; refuse `interrupt:true`; remove false/absent legacy `interrupt`, inner `detach`, `notify`, permanent `id`; set absent kind to `follow-up`; preserve all other template metadata/settings verbatim; set outer detach false. Preserve SQL IDs and every schedule/last-run/outcome field. Update all affected jobs in one `BEGIN IMMEDIATE` transaction, validate readback/integrity, and preserve non-session job JSON bytes. Record removed notify targets for review—do not replace them with an invented answer-forwarder.
5. Stage target host and all affected plugin components from the resolved manifest as one offline cut. Do not call live `core.activate` for piecemeal contract upgrades. Unaffected plugin state/binaries stay intact. Copy only SDK `src/lib.rs` for this selected baseline; preserve relocated world Cargo manifests, imports and `interface`. If hashes differ from this baseline, resolve the coupled source-copy set rather than applying this one-file assumption blindly.
The native host/runtime installation itself remains gated on the unresolved owner/architecture/closure in step 0. The rehearsal exercised real host shutdown plus offline plugin/SDK/boot replacement, not replacement of the live native service package.
Native host installation is an **outer launcher selection**, never an overwrite of immutable `/nix/store`. While stopped, install the static selector flake recorded in the manifest at `/home/surma/sau-384-cutover/target-selector/flake.nix` (outside the world). The proposed parent was absent at read-only inspection; refuse an unexpected existing image rather than overwrite it. Its sole app points to the exact target package's `bin/sausage`. Hash-guard and back up any existing selector; do not overwrite an unknown image. The selector is target launcher configuration, not a migration executable. The actual old Nix package/loop → offline plugin/SDK/boot swap → new Nix package/loop was rehearsed; neither public remote refresh nor a source-only host was substituted.
## 3. Install boot/caller configuration while stopped
@@ -57,7 +59,14 @@ Resume an interrupted **offline** swap only from its saved plan: each file must
## 4. Start/reconcile and verify
Start the pinned target external owner once. Observe readiness/completion events, not process/endpoint polling. Confirm:
Checkpoint `started` **before any launch attempt**. In the identified Herdr shell, mediator starts the same inspected loop exactly once with an explicit pinned selector:
```sh
SAUSAGE_FLAKE=path:/home/surma/sau-384-cutover/target-selector \
/nix/store/pz2b94a2v99r3msljdz82h7n3i81hhh5-sausage-loop/bin/sausage-loop /home/surma/world/
```
The selector directory must contain exactly the recorded installation bytes from step 2, not a mutable public flake. This pins crash restarts to the same reviewed host instead of refreshing unknown future code. Keep that selector/closure reachable for the target owner after deleting the migration tool. Observe boot/RPC/watch completion events, not process/endpoint polling. If shell/launch acknowledgement is lost, inspect once before retrying; never launch a second owner blindly. Confirm:
- one manager, one ownership lock per running session; no old mail deliverer, old orchestrator service or old session agent owners;
- cred starts **locked**, with no master-key/vault handover; stored grants/requests/settings remain intact; interrupted credential request callers retry their same stable key, not a secret-backed external mutation;