Idunn/docs/route-continuity-cut.md
Meta Vens f1b45666c6 Route map: B5 merged (fcb8ba1) and installed on Yggdrasil
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:36:02 +02:00

167 KiB
Raw Permalink Blame History

Route continuity and admission: cut map

Status: cut map, Imagination pass 0b (Opus), 2026-09-29. Nothing has landed. 2026-09-30: the Odin-lifecycle authority audit is folded in (sections 3, 4 and 5); see the Cut status block. Ends are owned by route-continuity-target.md. 2026-09-29, Self (Eureka session "Codebase audit"; the campaign was handed over from the StreamPixels deployment session with the operator's confirmation). The operator ruled all six questions in section 3, one at a time: Q1 (b), Q2 (b), Q3 (b), Q4 (b), Q5 (a), and Q6 (a) with a Soul gate on the weekend branch before the merge. Next: S1 in Hands, and the Q6 Soul gate on CultLib codex/fix-node24-ajv-esm. Behaviour cuts re-take their file:line anchors in their own briefs. The operator has asked for no live mitigation on Yggdrasil yet.

B3 status, 2026-09-30 (Self). Hands pushed B3 to idunn/route-b3 at 5ca7760; it is based on B1's merge a61540b, not stacked on B2. The Opus Soul pass found:

  • Full suite green: 252 passed, 2 ignored.
  • cargo-mutants --in-diff: 31 caught, 4 missed.
  • Soul's probe P1 drove a stateful route-proof target through the Engine, so the stateful path works; it was only untested.

Verdict: safe to merge for the StreamPixels web ship. The fix batch, in Hands, covers these findings:

  • the evidence tag is a second class authority, and a pre-B3 transaction wedges Routing;
  • the driver's rejections of a bad answer become gate waits instead of errors;
  • the shortfall is prose;
  • OdinSelf is decorative, and "is Odin" is still decided by name in five places;
  • Warming is never driven past one step;
  • seven unpinned checks.

Before idunn up, one read-only decode of a copy of the live control.cc checks for non-terminal transactions of routed targets with no Odin declaration (operator approved, 2026-09-30).

B3 fix batch, 2026-09-30. Pushed 6ea824b and e11b457; 275 tests pass.

  • ReadinessClass::of is the only class authority. The evidence tag is demoted to Voucher.
  • ChallengeFailure::{Silent, Refused} separates silence from a refused answer.
  • PresenceDisagrees makes the capacity shortfall a typed value.
  • The recipes are on route/b3-declare: Heimdall d5acc94, Muninn 2260853.
  • Still open, for a fresh Hands: three survivors (boot re-proof by tag, config error as silence, first-Odin check by name) and cargo mutants --in-diff.

Live store, read-only decode of a copy, 2026-09-30 14:56 (operator-approved).

  • control.cc holds 5 envelopes, all admitted_generation.v2 (pre-F0), and no transactions.
  • history.cc holds 957 transaction v3 envelopes and 641 command v2 envelopes.
  • Admitted generations, all on Odin receipts:
    • ghostlight: routed, declares Odin, stateful.
    • odin: routed rudp, provides the rendezvous.
    • raven-muninn: unrouted, no declaration, stateless. B3 class Undeclared, so held.
    • streampixels-service: routed, declares Odin, stateful.
    • streampixels-web: routed, declares Odin, needs streampixels.service.api.
  • Heimdall is not admitted. It drops out of the ship window. Its declaring recipe still merges. Its first deploy will hit Soul pass 2's F2: a routed, Odin-correlated target must pass the stable-route proof, and Heimdall answers no challenge. Record that for its own deploy.

B3 pass 2 (Soul, 86ee21d): safe for the web ship. One same-window blocker:

  • F1: supervision mints a held continuity over a dead held generation, and that continuity owns the target, so the declaring redeploy can never freeze. The fix is in Hands.
  • F5, bad answers read as Silent over HTTP keep-alive and TCP, is in the same batch.

B3 fix batch (86ee21d..e8b4487), Soul pass 3, 2026-09-30: merge with follow-ups. Nothing here hurts a live host on install. 294 passed, 2 ignored. cargo mutants --in-diff: 53 caught, 0 missed.

  • F1 holds. There is one continuity mint site, and the hold check comes before it. A declaring redeploy replaces a dead held incumbent end to end.
  • F5 holds. Zero bytes is Silent. Any byte then a stall or stop is Refused. Non-2xx responses and non-chunked Transfer-Encoding are Refused.
  • Follow-ups, in Hands on the same branch:
    • the P1b test never reaches the yield branch (mutant M7 survives);
    • the chunked trailer loop is unbounded;
    • +N lengths and whitespace before the colon are accepted;
    • exact 1 MiB and one-byte-then-stop are unpinned;
    • the Silent doc is stale;
    • the dead-hold report goes to stderr only and never clears.

Ruling, 2026-09-30 (operator): cancel accepts a held pre-fence Deploy. Idunn still never aborts a held record on its own. An operator can discard a held Deploy that has not reached the fence with idunn cancel. Nothing is fenced, so the incumbent is untouched.

B3 follow-ups landed on idunn/route-b3 at 5c49646, 2026-09-30.

  • Cancel accepts a held pre-fence Deploy. It shares pre_fencing_abort_intent with the scheduler.
  • The P1b test is now in direct-supervision form, with a positive control.
  • The reader is strict: trailers share the 32 KiB header budget; digits only; header names without whitespace.
  • The Silent doc is corrected.
  • Tests: 301 pass. a_failing_step_is_retried_after_a_backoff_not_every_tick is flaky under a full parallel run, on the base too.
  • cargo mutants --in-diff: 24 caught, 0 missed.
  • A narrow Soul pass gates the install.

Ship log, 2026-09-29 (host clock).

  • B3 merged at d32395a and installed on Yggdrasil at 18:36 UTC, with sha 854aa351. It migrated 5 control records. The store backup is /root/idunn-store-backup-20260929T183604Z-pre-route-b3.
  • Nginx reloads fell from 82 per 10 minutes to 0. Odin continuity timeouts fell from 92 in 6 hours to 0.
  • Raven's actuator (built 2026-09-11) could not decode the new plan types. A rebuild then found that the Windows idunn-host has not compiled since S1: FROZEN_SOURCE_SYMLINK_TARGET_LIMIT was defined for unix only. That is fixed at d3db582.
    • The new actuator (sha f36bf1fc) is swapped in on Raven. The old one is kept as idunn-host.exe.prev-20260929T190006Z.
    • Follow-up: nothing builds the Windows actuator in verification.
  • raven-muninn was admitted at 19:02 by Odin correlation, replacing the held generation.
  • The streampixels-service deploy failed before fencing. The S1 bulk_fetch_objects asks for bare blob ids with git fetch --stdin. In a blob:none clone, the connectivity check then fails with "bad revision ". The fix, with a test, is in Hands on fix/bulk-blob-fetch: a promisor-style fetch with --filter=blob:none and fetch.negotiationAlgorithm=noop.

Ship completed, 2026-09-29 (host clock).

  • Bulk-fetch fix. Merged at 583b2a7 and installed at 19:17. The Idunn sha is 01bde676.
  • The first streampixels-service redeploy failed after fencing. The incumbent was restored, and every restart was refused by Odin with "runtime presence publisher sequence was reordered". Cause: Odin compared publisher sequences across incarnations, and the TS publisher counts from 1 per process.
    • The operator ruled: fix Odin's rule.
    • Odin db041ec compares sequences only while the stored presence still authenticates under the current activation. Soul said deploy, with follow-ups; Odin was deployed at 19:4x.
  • Admissions.
    • streampixels-service was admitted at 19:50 on s3-c2.
    • The web binding dropped STREAMPIXELS_ODIN_CULTMESH_RUDP, and gamecult-ops 37b5ed9 does the same.
    • streampixels-web was admitted with Odin-free readiness.
  • Public cutover at 19:54. The vhost upstreams moved to :8833 (web) and :8832 (service). The backup is /root/streampixels.gamecult.org.conf.bak-20260929-195415-pre-idunn-upstreams.
    • https://streampixels.gamecult.org/api/catalog returns 200 (120,893 B), after being 502 since migration 013.
    • The overlay SSE stream answers text/event-stream, and the ops checker passes.
  • Legacy units. streampixels-web.service is disabled and stopped. streampixels-service.service is disabled, and was already stopped.
  • Follow-ups.
    • The C2 acceptance hour: sample the nginx reload counter at T0 and T0+60, with no rejected challenges.
    • Nothing builds the Windows idunn-host in verification.
    • Odin presence: pin the stored-at re-authentication time (Soul's M3 survived), and consider checking activation identity directly rather than any failure to authenticate.
    • The dead-hold report field (ruled).
    • B2 rework on B3.
    • Odin's CultLib pin bump, after the stray-packet merge.

C2 acceptance hour passed, 2026-09-29 19:55-20:55 UTC (21:55-22:55 CEST).

  • 1 nginx reload, which was Odin's own route promotion during its 5c37860 deploy.
  • 0 rejected StreamPixels challenges, 0 Odin continuity timeouts, and Idunn NRestarts=0.
  • One running unit each for service, web and Odin. Public / and /api/catalog return 200.
  • Also deployed in the window: Odin 5c37860, carrying CultLib 3bf1c0c (RUDP stray-packet hardening in every runtime), Sleipnir removed, and presence ordering decided by activation identity.
  • Still open for Odin: it exits when its 64-slot RUDP session table fills, because its own heartbeat publish fails. Publishers that never Disconnect hold slots for 30 s. The fix is in Hands.
  • The second run, with Odin broken on purpose, remains the operator's to schedule.

Ruling, 2026-09-30 (operator): the dead-hold report is stored on the generation.

  • Add an optional last_error to AdmittedGeneration (new key 21), and make status render admitted generations.
  • It is cleared when the generation is replaced or its hold is released.
  • The generation:<target> readiness report gets the same home.
  • It is queued after the ship, with the B2 rework.

Odin diagnosis (Eyes, read-only, 2026-09-30).

  • The 109 Odin route timeouts are self-inflicted. Live Idunn (8ae00a1, pre-S1) reloads nginx inside restore_admitted_membership before every Odin challenge. The reload moves the reuseport UDP flow, so the challenge misses. All 109 have a reload 3.05-3.12 s before them.
  • 2,977 reloads in 6 h. 479 shutting-down nginx workers, because the stream has no proxy_timeout.
  • The route timeouts do not block Ready correlation.
  • Odin has crashed twice right after a reload. A stray RUDP packet with a foreign connection id escapes cultnet-rs rudp.rs:1217 (require_connection) and ends the process. Each crash costs about 75 s without a lease. The CultLib fix is in Hands on hands/rudp-stray-packets. Odin takes it at its next pin bump, after the ship.

Operator ruling: Idunn first. Install the new Idunn (B3 carries S1), then watch for about 15 minutes that reloads stop and Odin stays up. Only then redeploy raven-muninn and StreamPixels.

Ship sequence:

  1. B3 fix, then narrow Soul, then merge B3.
  2. Merge StreamPixels route/s3-c2 and the Muninn and Heimdall route/b3-declare recipes.
  3. Build the Idunn release on its deploy path.
  4. Back up control.cc and history.cc. F0's lift is irreversible.
  5. Install Idunn.
  6. idunn up raven-muninn with the declaring recipe.
  7. idunn up streampixels (service, then web).

Rulings, 2026-09-30:

  • A stored record whose evidence disagrees with its declared class is held and reported, never aborted by Idunn. B5's deadlines or the operator resolve it.
  • Heimdall and raven-muninn are redeployed with their declaring recipes in the same window as Idunn B3 and the StreamPixels ship.

Ruling, 2026-09-30: the recipe declares readiness; Idunn infers nothing. This settles the fork Hands raised against a literal Q1(b). An unrouted target whose recipe declares neither a route nor an Odin dependency is refused at admission with a typed error. It does not fall back to Odin correlation because it has no route. raven-muninn and Heimdall both publish to Odin, so their recipes declare it. The rejected alternative let Odin's availability gate Muninn's crash recovery through an undeclared, inferred dependency.

B2 status, 2026-09-30 (Self). Hands pushed idunn/route-b2 at 1786ddf. The Opus Soul pass rejected it for merge.

  • F1 (critical): ContinuityBackoff.deferral_reason was added without a schema bump. B2 fails the canonical re-encode of every three-field v3 generation that F0, B1 or B3 wrote, so Idunn cannot read its own store.
  • F2: the ceiling refuses a rollback, which leaves an unproven candidate on the stable route.
  • F3: a failed challenge writes, and continuity's CAS then loses against a stale envelope.
  • F4: neither window rolls; both are anchored at the first charge.
  • F5: a clock stepped backwards stalls everything for the length of the step.
  • F6: record_proved_challenge is unpinned.
  • F7: the mount preflight is not gated.
  • B2 also does not compile on top of B3, and after a one-line fix two tests fail, because the fixture target is now route-proof.

The rework is a fresh branch from 583b2a7, not a rebase of 1786ddf. Its map is under B2 in section 4.

Rulings, 2026-09-30.

  • The ceiling never refuses a rollback. Restoring the admitted or incumbent route is survival, not deployment. It is always allowed and always counted. Only installs and other forward changes can be refused. A rollback that keeps failing is spaced by backoff, never refused.
  • First routed deployments are metered now. The actuation window lives on the target, not on the admitted generation, so a target with no admitted generation is charged too. That schema change is folded into the store-version bump that F1 needs: a v4 generation, with a typed lift from v3.

Rulings, 2026-09-30 (operator, on the B2 rework map).

  • 3d A (2026-09-30, morning handoff): separate budgets. The 12/h route actuation ceiling counts only Forward actuations (deploys). Survival actuations (continuity restarts, rollbacks, repairs) are still recorded for visibility but do not charge the ceiling; each stays bounded by its own mechanism (the 6/h per-target restart log, the challenge backoff). A crash-looping target can therefore always be rescued by a deploy. Option D (reset the restart log when a new release is admitted) was not chosen: a new release still inherits its predecessor's restart history. B2 is merged (46d9c2f) but not installed; this follow-up (B2-3d) lands before the install.

  • B2-3d status (Self, 2026-09-30). Built on hands/b2-3d (e9c718e, 057bdd5). Soul re-ran verify.sh clean (341) and killed every one of its own mutants on the Forward/Survival split. But the premise the operator ruled on is false: "every Survival path has its own bound" does not hold.

    • F1: a continuity transaction whose route proof keeps failing retries inside one transaction under the 60 s resume cap, reloading nginx twice per retry. That is about 120 reloads/h per target, with no end until B5.
    • F2: a failing post-fence withdrawal reloads about 60/h. restore reloads even when the fragment is already correct.
    • F3: nothing bounds reloads across the host.
    • None of these is new in 3d: Survival was never refused before either. The live Idunn, which predates B2, has no reload bound at all. What 3d adds is the false claim, at control_plane.rs:76-81, plus the admitted Deploy doubling to 24/h.
    • The fix belongs to B5 ("every post-fencing phase ends"), plus a restore/install that skips the reload when the fragment is already correct. The operator was told on 2026-09-30.
  • B2 + 3d installed (2026-09-30). The operator accepted recommendation A: install now, with B5 next.

    • Installed at 15:22 CEST: a96ad9d, idunn sha256 848c08e5...; the replaced binary was 01bde676.
    • The migration moved 5 control records to deployment_transaction.v4 / admitted_generation.v4.
    • Pre-install backup, taken with Idunn stopped and byte-compared: /root/idunn-store-backup-20260930T132204Z-pre-route-b2/ (control.cc 5f879f55..., history.cc bafa76ab..., the other stores, and the old binaries).
    • Preflight: 690 transactions, all terminal. They were identical after the install.
    • No managed target's PID changed. Raven reattached after 15 s.
    • After 10 min: no odin route-continuity rejections (about 95 in the previous 3 h), 0 nginx reloads, and the Odin post-deploy check fails=0.
    • B5 and skip-unneeded-reloads are in Hands (hands/b5).
  • B5 Soul pass 1 (2026-09-30, on a96ad9d..c06ff2b): merge after the batch-2 fixes; no install before them. Soul reran verify: 352 passed. Batch 2, now in Hands, fixes:

    • A (high). A continuity aborted from Prepared leaves a lease on disk that driver.grant wrote before Granted was durable (control_plane.rs:6916-6930, :9081). The next fencing refuses, and the target is wedged.
    • B (medium, a regression from 6ff7d5a). The reload skip hides a crash between the write and the reload on the restore/install transaction paths, leaving nginx proxying to a dead port. The skip goes back to supervision only.
    • E. The resolver's command filter is unpinned.
    • F. Odin-correlated adoption has no committed test; mutants M6 and M7 survive.
    • J. A flaky backoff test (:14200).

    Recorded:

    • C (the abort has no deadline) is closed by the Q3-ii cut.
    • D. There is no host-wide reload bound. 170/h is about a third of the F6 storm, and 4830af8 lowers the real worst case.
    • G. A newer Ready can rewrite lease_adoption. It names the same lease, so it is harmless.
    • H. AwaitingReady and Routing keep the F16 shape. It is unreachable at today's heartbeats.
    • I. An idunn expire request written while Idunn is stopped lingers, and there is no verb to withdraw it, which is a downgrade hazard.
  • B5 MERGED and INSTALLED (2026-09-30). Merged to main at fcb8ba1 (Soul passes 1-3). Built on Yggdrasil: /srv/build/idunn-fcb8ba1, 360 passed. idunn sha256 e96bcefa..., idunn-provision 127902ef..., unit unchanged. Installed at 22:35 CEST.

    • Backup, taken with Idunn stopped and byte-compared: /root/idunn-store-backup-20260930T203515Z-pre-route-b5/. It holds control.cc fab5b66b..., history.cc a2627ea4... and idunn.running 848c08e5... (a96ad9d).
    • Preflight found one live transaction: the raven-muninn continuity 990dfc25 in AwaitingReady for about 40 min. Raven had rebooted, and its actuator timed out at 21:36 and 21:49 CEST. The operator ruled "Feel free to install".
    • After install:
      • No managed target's PID changed. Raven reattached after 15 s, and nginx made 0 reloads.
      • B5 aborted the stale continuity at its passed deadline, and supervision opened continuity-26737b91.
      • A single log line, "could not resume transaction tx-bcfbb693...: transaction disappeared while checking scheduler progress", appeared at 20:35:38 UTC. It looks like a same-tick ordering quirk between the resolver and resume. It goes to the next Soul pass.
    • Rollback rule: stop Idunn and check control.cc for any idunn.expiry_request envelope. If one exists, restore the backup instead of swapping the binary.
    • Named tradeoff (Soul batch-3 F1): a persistent ufw delete failure while aborting a first routed deploy now leaves the aborted candidate routed until ufw recovers. Before B5 the cost was reload churn. Q3-ii's abort deadline bounds it.
    • Test pins F2-F4 are in Hands (hands/b5-pins).
  • B5 batch 2 (c06ff2b..aef0da3) and Soul pass 2 (2026-09-30): merge and install after batch 3.

    • A, B, E, F and J hold. Verify: 356 passed. cargo-mutants caught 14 of 14 viable mutants, and Soul's hand mutants killed 16 of 20.
    • Batch 3, in Hands: test pins for MB2 (changes_route swapped for "no incumbent"), ME2/ME3 (the resolver filter by id length or first byte) and MJ2 (the computed backoff unpinned). restore withdraws the firewall rule before reloading when there is no prior route, so a failing ufw delete no longer reloads nginx each retry.
    • Hands corrected Self's brief: the continuity path's zero reloads came from 6ff7d5a's skip, not 4830af8. The narrow no-change case (changes_route() == false) keeps it without a write, so there is no crash window.
    • Recorded, not in B5:
      • P1: a crash between atomic_replace and the reload in supervision's restore_admitted_membership (drivers.rs:5543-5545). Idunn has no SIGTERM handler, so a stop in that window leaves nginx on the drift until something else reloads. This also exists in live a96ad9d. Fix with Q3-ii or a durable "reload owed" marker.
      • The abort revokes before it stops the candidate (control_plane.rs:8758 before :8783). Q3-ii owns the abort order.
  • Q-B2-1 (a): the continuity restart log moves into TargetSupervision; the restart ceiling reads the target's own log; the carry compensator is deleted.

  • Q-B2-2: fail at once. A deploy refused by the route/actuation ceiling before the fence fails immediately, with the reopen time in the error.

  • Q-R1 (a): the route declaration is the challenge declaration. Every routed target answers the stable-route challenge regardless of readiness class. R1 checks it before the fence; H1 gives Heimdall a responder. Operator note: the question's first framing was confusing; the cause is that Heimdall has no responder, not its class.

  • Q-B4: Idunn's own proof. Routed providers by current route proof and not degraded; unrouted providers by admission plus a live workload observation. One clause is held open: "a current Odin non-Ready word still excludes a provider" is not ruled, pending the Odin-lifecycle authority audit (a separate Imagination pass). Operator: "the last few rulings on Odin are making my authority sense tingle, it feels like Odin is doing lifecycle work when that's very much Idunn's wheelhouse." B4 is blocked on that audit.

Odin-lifecycle audit rulings, 2026-09-30 (operator). The audit landed as section 5. Q-O1: Idunn proves (supersedes Q1 (b); readiness classes end at A1 + A3). Q-O2: a private challenge endpoint for unrouted targets (A2 + a Muninn responder). Q-O4: the Odin dependency is optional and does not gate a compile. Q-O5: restart on bad auth (amends Q5 (a)). The held Q-B4 clause is struck; B4 becomes B4′. Q-O3 (redeploy order odin -> streampixels-service -> ghostlight -> web with the operator present, vs a one-shot relabel) is not yet asked and is needed before A1 installs. Odin keeps three permanent self-publication failures fatal until A3 + Q-O5 land (temporary rule, section 4).

Cut status, 2026-09-30.

  • Ready for Hands: B2 (rework map in section 4); D1 (after B2). R1 is never to be written, superseded by A1.
  • In progress: hands/idunn-b2-rework (B2 rework); hands/idunn-w1-t1-t2: W1, T1, T2; X1 on Odin hands/odin-x1-residue; the temporary fatal rule on Odin hands/odin-session-table.
  • Next, in order: A1 (after B2, and after Q-O3 is ruled), B4′, A2 with M1, A3 (irreversible store migration, back up first), O-R, L-R (section 4, Odin-authority cuts).
  • Open for Soul: the continuity-restart stall probe and the journal check of the ghostlight/raven-muninn exclusion (section 5.5).
  • Unblocked: B4 (now B4′). Blocked: H1 (Heimdall repo), on CultLib C1. A supervision change for Q-O5 has no cut yet.

Heads read for this map:

Repo Ref Notes
F:\Projects\Idunn main = d719485 (code 8ae00a1, trusted baseline 9ede4cd) idunn/cut1-fix5 is 14 ahead / 25 behind main
F:\Projects\StreamPixels main = a665bce vendor/CultLib gitlink = 542ddd3
GameCult/CultLib origin/main = 268e0ef; origin/codex/fix-node24-ajv-esm = 542ddd3 the Idunn TS runtime package exists only on that branch (see F-C1)
F:\Projects\Odin 379b826 read only, for lease pickup
Yggdrasil /usr/local/bin/idunn sha256 5f257f2c…, idunn-yggdrasil.service active read-only probes, 2026-09-29

"Read" means the claim comes from source at those heads. "Probe" means a read-only command on Yggdrasil on 2026-09-29. Nothing was written to the host.


1. Body facts

1.1 Admitted route supervision actuates on observation age

  • F1. The scheduler tick (run_scheduler_tick, control_plane.rs:3075) runs resume_one_transaction, then supervise_one_admitted_generation (:3293), then freezes commands. The loop sleeps poll_millis (default 500 ms, :1473) between ticks. Read.

  • F2. supervise_one_admitted_generation calls supervise_admitted_route (:3606) first for every admitted target that no transaction owns past Fencing. The call is made for every admitted target on every tick. Read.

  • F3. supervise_admitted_route returns early only when all three hold: the fragment on disk equals the admitted membership, the route observation is younger than topology_maximum_age_millis (default 30 000 ms, DEFAULT_TOPOLOGY_MAXIMUM_AGE_MILLIS, :61), and no repair intent exists. Otherwise it CASes route_repair_started_at_unix_millis = now into the admitted generation and returns (:3654-3673). On the next tick it calls restore_admitted_membership and then prove_stable_route_against. It clears the intent only after a successful proof (:3707-3722). Read.

  • F4. restore_admitted_membership (drivers.rs:5455) runs the following on every call, even when the fragment is byte-identical:

    1. validate_candidate_in_private_mount, which launches a transient systemd-run --wait unit running nginx -t.
    2. admit_endpoint, which runs ufw allow.
    3. reload() (:5254), which runs nginx -t and then systemctl reload nginx.

    The write is skipped when the bytes match (:5467). The actuation is not. If the reload fails, it deletes the fragment. Read.

  • F5. Consequence: a healthy routed target reloads nginx about once every 30.5 s. A target whose proof fails keeps the repair intent and runs restore and reload on every tick, limited only by the 3 s challenge timeout. Nothing records, rate-limits, or backs off these actuations. Read.

  • F6. Probe: 551 Reloaded nginx.service in the last hour across four route fragments (ghostlight, odin, streampixels-service, streampixels-web). 460 processes named nginx: worker process is shutting down. The Idunn journal for the last 10 min shows route continuity rejections for streampixels-service (EAGAIN, 6), streampixels-web (EAGAIN, 4; HTTP 503, 1), and odin (RUDP connect timeout, 1).

  • F7. Probe: /etc/nginx/idunn-stream-routes/odin.conf is the Idunn-rendered UDP stream proxy listen 10.77.0.1:17871 udp reuseport; → server 127.0.0.1:17973;. render emits that shape for NginxStreamUdp+rudp (drivers.rs:5210-5217). Odin's own route proof goes through that listener as a RUDP catalog snapshot (request_runtime_presence_at, drivers.rs:5566-5574). Every reload of any target therefore moves Odin's UDP flows. Read + probe.

  • F8. An admitted generation is never withdrawn. The only writers of AdmittedGeneration::TYPE are commit (:4749), route supervision (:3670, :3715), and topology refresh (:3780). After three refused continuity restarts, supervision stops restarting (:3522-3528) but keeps supervising the route of a dead incarnation. Read.

  • F9. The continuity restart limit counts refused restarts per generation (is_refused_restart, :3469-3488). A restart that succeeds admits a new generation and resets the count. A unit that starts, is admitted, and then dies is restarted without bound and without backoff. Read.

1.2 Web route proof today

  • F10. Idunn's challenge is a CultNet SnapshotRequest sent to the stable endpoint. For http it is POST /cultnet/snapshot with MessagePack and an explicit Content-Length. There is a 3 s connect/read/write timeout, and ConnectionRefused is retried for 2 s (29946bb) (drivers.rs:5524-5580, :5628-5720). Read.

  • F11. authenticate_routed_presence (control_plane.rs:5185) verifies the following through authenticate_runtime_presence_claim and correlate_runtime_presence_claim (CultLib 61549aa, runtime_authority_contracts.rs:1163-1230):

    • the provider signature;
    • the activation signature, made with the per-launch key whose public half is in Idunn's activation;
    • the Expected, activation, runtime and endpoint binding;
    • observed_at >= challenged_at;
    • age at most 30 s;
    • state == "active";
    • detail == "route-observation:{message_id}";
    • the exact current write-lease sha.

    The Idunn verifier already satisfies invariant 4 over HTTP. It needs no Odin input. Read.

  • F12. The app side couples that proof to Odin. publishRouteObservation (StreamPixels vendor/CultLib/packages/cultnet-ts/src/idunn-runtime-authority.ts:286-298) calls publish("active", "route-observation:<id>"). That call signs the presence and then publishes it to Odin over RUDP, waiting up to 5 s for accept and 2 s for ack (publishDocument, :319-356), before it returns the document. It signs active regardless of application health. Read.

  • F13. The Next handler (apps/web/app/cultnet/snapshot/route.ts) maps any publisher failure to 503. The app-side RUDP wait (up to 7 s) exceeds Idunn's 3 s read timeout, which produces the EAGAIN (os error 11) in F6. Read + probe.

  • F14. The app requires Odin before it can prove anything:

    • loadIdunnRuntimeAuthorityFromEnvironment throws unless Expected carries a shared-infrastructure odin.verse-rendezvous dependency equal to STREAMPIXELS_ODIN_CULTMESH_RUDP (idunn-runtime-authority.ts:114-118).
    • runIdunnRuntime blocks startup for up to 30 s on Odin accepting the initial warming publication (deployment/idunn/runtime-presence.mjs, publishInitialWarming).
    • deployment/idunn/web.toml declares the Odin dependency and the env var.

    Read.

  • F15. The Idunn runtime TS package (idunn-runtime-authority.ts and its test) does not exist on CultLib origin/main. It exists only on the unmerged branch codex/fix-node24-ajv-esm, which is 10 commits ahead of origin/main (0bedcde..542ddd3). StreamPixels pins that branch commit. Read.

1.3 Which Idunn decisions consume Odin for a non-Odin target

All of these are read:

Decision Site Odin input
Provider selection at plan compile advance_sealing :3825 → current_ready_provider_tokens admitted providers' Odin ready and latest_odin_observation; ManagedReady carries odin_topology_correlation_sha256 and sequence (deployment_plan.rs:136-145)
Provider currency (Deploy only) Sealing→Starting :3880, Starting :3945, Routing :4527, Committing :4629, :4673 → validate_selected_providers_current same receipts; since 9f00e7a, authenticated at their admission time rather than now
Warming advance_warming :4028-4103 only Odin correlation, except the FirstOdinDirect exception for target == "odin". For a non-Odin target with no correlation it records a gate wait forever
Lease grant (stateful) fresh_warming_for_lease :4808 new Odin sequence plus new signed presence
Ready advance_awaiting_ready :4423 Odin correlation is_semantic_ready
Route admission advance_routing :4487 admits the latest topology; Ready is refreshed and require_current=true
Commit advance_committing :4599 topology admitted twice, once before and once after the final stable-route proof; AdmittedGeneration requires ready, latest_odin_observation, odin_authority and the cursor as non-optional fields (:1322-1331)
Admitted refresh refresh_admitted_topology :3726 Odin correlation updates the admitted receipts
Graph gate web recipe's odin.verse-rendezvous dependency Odin must be admitted and Ready to compile the plan
  • F16. Commit livelock (source-read, supported by live history).

    1. advance_committing admits the topology, proves the stable route, then calls admit_latest_topology(&ready_current, …) again.
    2. If a new Odin sequence arrived meanwhile, that call persists it, the envelope changes, and the function returns Ok(()) without committing (:4658-4666).
    3. The web's route proof itself publishes a fresh active presence to Odin (F12), and Odin re-stamps its correlation when the presence changes (comment :5473-5478).
    4. Every commit attempt therefore tends to manufacture the evidence that aborts it.

    Probe: up-efb85890… (tx-5fe84313…) is now Complete/Admitted at Odin cursor 4008. It had been Committing for more than 12 h at cursor about 3680. A commit that eventually wins a race fits this mechanism. No reproducer has been written yet.

1.4 Post-fencing phases and leases

  • F17. When a transaction errors at or after Fencing, it is aborted only if candidate_is_permanently_stopped. Otherwise record_resumable_error runs (:3107-3131). There are no deadline fields on DeploymentTransaction (:651-731). Read.

  • F18. advance_awaiting_ready returns Ok(()) for as long as no new Ready correlation exists. Only candidate death interrupts it (observe_candidate_before_waiting). Routing and Committing wait the same way. Read.

  • F19. Lease pickup windows are owned by the consumer, not by Idunn:

    • Odin accepts a lease only if its warming_presence_sha256 names a warming proof Odin issued within WARMING_PROOF_LIFETIME_MILLIS = 60_000 (odin-daemon/src/main.rs:73, acquire_process_write_lease :886-911, try_activate :225-263).
    • The TS publisher accepts any of its last 64 proofs with no time bound (assertLeaseMatches, idunn-runtime-authority.ts:687-706).
    • Idunn neither knows the window nor observes adoption, other than through a later Odin correlation.

    Read.

  • F20. cancel (:1725) is an operator CLI process that writes post_fencing_abort directly into a live daemon-owned transaction by CAS (8ae00a1). The only cases it accepts are Deploy, past Fencing, SkippedStateless, no completion. Continuity and stateful transactions have no recovery command. Read.

1.5 Continuity drift between the admitted receipt and the projection

  • F21. from_continuity (:783) reuses the incumbent's Expected, so the continuity candidate projects under the same key {target}@{expected sha}. publish_observed_activation replaces any activation under that key "without comparison" (drivers.rs:4927-4955). The admitted generation keeps the old activation until commit. Commit replaces the admitted generation and the transaction in one CAS (:4745-4770), so a successful continuity leaves no drift. Read.

  • F22. Drift source. Both abort intents set topology_reconciliation = Skipped for CommandKind::Continuity (begin_pre_fencing_abort :5822-5828, post_fencing_abort_intent :6224-6230). A failed continuity therefore leaves its own candidate activation projected under the shared key while the admitted receipt names the older activation. Two things then refuse, because ProjectedIncarnation::read rejects any projected activation that differs from the one passed in (drivers.rs:4293-4300):

    • supervision's pre-restart demotion (:3538-3556, logged and ignored);
    • a later deploy's post-fencing demote_to_expected_only.

    a148802 made rollback demote whatever activation is projected, provided it verifies against Idunn's anchor (demote_current_activation_to_expected_only, drivers.rs:4500-4543). The cause is the skipped reconciliation. The coherent cut is for the continuity abort to demote its own exact activation to Expected-only. Read.

  • F23. 9f00e7a exists because admitted receipts only advance when Odin emits a new sequence (refresh_admitted_topology). Provider publications to Odin were failing (F7, F12), so a provider's receipts aged past 30 s and dependents could not select it. The drift source is that dependency currency is sourced from Odin receipts at all (see Q2). Read.

1.6 Weekend commits

Commit Read Classification
a148802 recover fenced rollback from current activation diff Revert in Cut B1, together with the F22 fix. Delete demote_current_activation_to_expected_only and its test. Restore the doc paragraph to the exact-activation rule
9f00e7a replay provider receipts at admission time diff Revert in Cut B4, together with its replacement currency source (Q2). A revert alone re-breaks dependency selection
ae8ac45 process title as launch evidence diff Keep. command_line_sha256 stays launch-time evidence; Node's process.title rewrite is not identity drift
29946bb wait for stable route listener diff Keep. After Cut S1 it covers only install-time listener handoff, which is its honest scope
8ae00a1 cancel live stateless candidates diff Delete in Cut S2 once deadlines exist. It is a second writer into daemon-owned transaction state (F20)
8651906 dynamic primary group diff Keep. Independent of this campaign
55e5d55/7a61c77, 4200330/e0f5147 log Self-reverted pairs; net zero

2. Identity, lifecycle, authority

An empty or "none" cell is a finding. It is marked (gap).

Kind Identity Lifecycle Authority
Admitted generation (idunn.admitted_generation.v2, control.cc) key = target; injective per target. generation_id = generation-{tx} Created/superseded by the commit CAS (transaction + generation atomic). Revised by route supervision (routing receipt, repair intent) and topology refresh (Odin receipts, cursor). Withdrawn: never (gap, F8). Replayed: read on every tick; validate_durable_authority at boot Owner: Engine commit. Three in-process writers (commit, supervise_admitted_route, refresh_admitted_topology), all CAS. Forbidden: CLI, Odin, drivers
Route observation / receipt (RoutingEvidence::Promoted inside transaction and generation) route_id + runtime_instance_id + membership_sha256 + signed_presence_sha256; injective per incarnation Created at Routing. Re-created by each successful admitted challenge. Stale at 30 s, which today triggers actuation (F3). What is in force afterwards: the last successful receipt, even while proofs fail Owner: route supervision (observe only, after S1). Forbidden: reload path, Odin
Route repair intent (route_repair_started_at_unix_millis, gen key 18) per admitted target; a single timestamp Set when stale or drifted. Cleared on proof. Attempt count, backoff, ceiling: none (gap) Owner today: supervision. Replaced in F0 by route supervision state (below)
Route fragment on disk (binding.config_path, /etc/nginx/idunn-stream-routes/<route>.conf) + ufw allow path is injective per binding (validate_route_binding_set :2386-2393); first line # Idunn Expected <sha> Written by install (Routing), restore_admitted_membership (supervision), and restore (abort, rollback). Removed on failed restore reload or last withdrawal. ufw is re-admitted on every restore (F4) Owner should be the route driver, acting only on membership change. Forbidden (after S1/B2): observation freshness, proof failure
nginx reload (process actuation) none; not a record (gap: no durable actuation ledger, so the invariant 2 ceiling cannot survive an Idunn restart) Unbounded (F5, F6) Owner: route driver. Rate authority: none (gap). F0 adds it
Deployment transaction (idunn.deployment_transaction.v3) tx-{uuid}; one live per target (blocks_new_target_mutation) Phases Sealing…Complete. Aborts pre and post fencing. Archived to history when terminal. Phase deadlines: none (gap, F17-F18). Post-fence error = resumable forever Owner: Engine. Forbidden writer present: idunn cancel CLI (F20)
Activation (in transaction/generation + projection idunn.runtime_activation in topology.cc) projection key {target}@{expected sha} is not injective across continuity: every restart of one Expected shares it (F21) Published after workload observation. Replaced without comparison. Demoted on death or abort. Continuity abort leaves its own activation projected (gap, F22) Owner: transaction for its own activation; generation for the admitted one. Forbidden: rollback adopting an activation it did not issue for this transaction (a148802)
Write lease (file record_path + .lock) + projection exact record binding Expected, activation, warming sha, epoch Prepared → Granted → (adopted by process) → revoked at fence or abort. Adoption is not observed by Idunn and has no pickup deadline (gap, F19) Owner: Idunn lease driver. Pickup-window contract: owned by consumers (Odin 60 s, TS 64 proofs). No Idunn-side owner (gap)
Runtime presence doc (gamecult.runtime_presence_health.v2) target + runtime_instance_id + publisher_sequence; the challenge nonce rides in detail Minted by the app per challenge and per heartbeat. Idunn persists only signed_presence_sha256 (route receipt) or the full bytes (FirstOdinDirect). Odin persists its own copy Owner: the service (provider key + Idunn-issued activation key). Forbidden: Idunn manufacturing it; publication to Odin being a precondition of the route answer (F12)
Odin correlation topology (/var/lib/gamecult/odin/idunn-runtime-topology.cc) {target}@{expected sha}, publisher sequence per signer Written by Odin whenever facts change. Idunn persists admitted sequences into the transaction/generation and advances a per-target cursor Owner: Odin (discovery). Consumer authority today: warming, Ready, dependency currency for every non-Odin target (§1.3). After this campaign: observation for route-proof targets, never admission
Continuity restart history derived by counting failed continuity transactions per generation (live + history) Resets on each new generation (F9). Per-target ceiling and backoff: none (gap) Owner: supervision
New in F0: readiness evidence enum on transaction and generation: OdinCorrelated(TopologyEvidence) or RouteProof(RuntimePresenceEvidence); the class is derived from Expected (Q1) Replaces the mandatory ready, latest_odin_observation, odin_authority and cursor for route-proof targets Owner: Engine. Forbidden: a route-proof target's admission reading Odin
New in F0: route supervision state per admitted routed target: last_challenge_at, consecutive_failures, next_challenge_at, and a rolling actuations window (count + window start) Revised only by supervision. Carried across continuity commits. Replaces route_repair_started_at Owner: supervision. Forbidden: any reload path that does not consult it
New in F0: phase deadline per transaction: phase_entered_at + deadline_at, from binding-owned durations frozen into the compiled plan (Idunn defaults) Set on every phase transition. Expiry is resolved by the deadline resolver (B5) Owner: Engine transition primitive. Forbidden: resume paths extending it
New in F0: lease adoption evidence the candidate's signed presence carrying write_lease_sha256 == granted sha, obtained directly or through Odin Recorded once in Leasing/AwaitingReady. Its absence at the deadline means the lease was not adopted Owner: Engine
New in F0: stateful terminal record completion variant naming the recovery (RestartAdmitted, RestoreIncumbent, OperatorRequired{reason}) Terminal, archived like other completions Owner: deadline resolver
New in F0: continuity backoff per target: attempts in window, next_restart_at Carried across generations. Replaces per-generation counting for the ceiling Owner: supervision
HTTP route proof no new kind. It reuses the presence v2 contract and RouteObservation (F11). RuntimePresenceEvidence gains a route-proof use beside FirstOdinDirect — —

3. Operator questions

Q1. Readiness authority for a CultMesh-aware service such as streampixels-service.

RULED (b) by the operator, 2026-09-29. HISTORY, not live design: superseded 2026-09-30 by Q-O1 (Idunn proves; readiness classes end at A1 + A3). The text below is kept as the record of the ruling.

Options:

  • (a) Every non-Odin target proves Warming and Ready directly through Idunn's challenge. Odin only observes.
  • (b) A target's class follows its own declaration. A target that declares a shared-infrastructure odin.verse-rendezvous dependency is Odin-correlated, as today. A target that declares none is route-proof.
  • (c) A new explicit binding field chooses the class.

Recommendation: (b). It reads the ruling literally: GC infra that advertises into the Verse is correlated by Odin, and a web app that declares no Odin dependency cannot be gated on one. It needs no new field. A repository choosing route proof gains no privilege, because both classes are signed by the same Idunn-issued launch key.

Depends on it: the F0 enum shape, the B3 scope, whether the service recipe changes (under b it does not), and whether the F16 livelock matters for Odin-correlated targets (B5 covers that).

Q2. Where a dependent's provider currency comes from (the 9f00e7a replacement).

RULED (b) by the operator, 2026-09-29.

Options:

  • (a) The provider's Odin receipts, authenticated now. This is a strict revert, but the web then depends on Odin transitively through streampixels-service, which breaks invariant 3.
  • (b) Idunn's own current route observation of the admitted provider: a successful challenge within max age, with capabilities taken from the signed presence in that proof. Unrouted providers fall back to their Q1 class evidence.
  • (c) Always the provider's own class evidence.

Recommendation: (b). Idunn already challenges every routed target, and under invariant 1 that challenge becomes the steady-state heartbeat. It is current, it is signed by the provider's launch key, and it needs no Odin.

Depends on it: B4, the ManagedReady shape (the Odin sha and sequence become a class-tagged evidence digest), and plan compatibility with existing admitted dependents.

Q3. How a stateful post-fencing deadline resolves once the candidate may have written.

RULED (b) by the operator, 2026-09-29.

Options:

  • (a) Always a terminal OperatorRequired record.
  • (b) Split on lease adoption (F0 evidence):
    • Not adopted: revoke, stop, and resolve like stateless. A deploy restores the incumbent through the fence. A continuity counts one refused restart.
    • Adopted: a deploy gets a terminal OperatorRequired{adopted-lease} record. A continuity gets a terminal record and the admitted release restarts under the continuity backoff, because it is the same release and state contract.
  • (c) Automatic incumbent restore whenever the state cut is declared reversible.

Recommendation: (b). Adoption is signed evidence that the candidate may have written. (c) trusts a declaration over evidence.

Depends on it: B5, the recovery variants of the terminal record, and the replacement for idunn cancel.

Q3 follow-ups from B5 Hands (hands/b5 c06ff2b), 2026-09-30:

  • Q3-i: does an issued lease with no adoption evidence count as possibly written? RULED yes by the operator, 2026-09-30. A candidate cannot prove it holds the lease while warming, and streampixels-service migrates in exactly that window. So any issued lease on a deploy is treated as possibly adopted.

  • Q3-ii: after an adopted-lease deploy expires, what stops continuity from restarting the incumbent on state the candidate may have migrated? RULED 2026-09-30: D before Ready, E after. Operator, verbatim: "D before Ready, E after; this actually closes the hole, and is a nice feature for Idunn to have".

    • D, before the candidate reports Ready: after the fence and before the write lease is issued, Idunn snapshots the target's state through a per-target snapshot/restore declared in its binding. If the deadline expires before Ready, Idunn restores the snapshot and brings back the incumbent. A failed restore is the only terminal OperatorRequired.
    • E, after Ready: the candidate is known-good and may have served traffic, so an expiry rolls forward. The candidate stays the release supervision keeps alive, and Idunn completes or retries routing and commit.
    • Assumption Soul must falsify: the incumbent cannot write between the fence and the lease, so the snapshot is exact.
    • Mapped: section 4, "Q3-ii cut: snapshot before Ready, roll forward after" (Imagination, 2026-09-30); it establishes the assumption for Idunn-managed writers and names what breaks it (Q3-ii.2).

    History, the question as first put: The operator on the offered hold-until-next-idunn up option, verbatim: "I don't like any of these options, unless a stateful deploy running out of time is an extreme edge case, in which case A". It is not extreme: the default awaiting_ready deadline is 300 s (deployment_plan.rs:474), so any stateful release that migrates and then fails to reach Ready lands here. Alternatives are being put to the operator.

Q4. The operator's recovery verb after 8ae00a1 is deleted.

RULED (b) by the operator, 2026-09-29.

Options:

  • (a) Delete it outright and let deadlines resolve everything.
  • (b) Replace it with idunn expire <command>. The command writes a typed expiry request that the daemon consumes, so the phase resolves through the same deadline resolver. The CLI never writes transaction fields.

Recommendation: (b). It keeps a human escape for long deadlines and removes the second writer.

Depends on it: S2 scope and the CLI surface.

Q5. Whether a sustained route-proof failure on an admitted target restarts its unit.

RULED (a) by the operator, 2026-09-29. Amended 2026-09-30 by Q-O5: an answer that does not authenticate as the admitted incarnation restarts the process under the continuity meter; silent answers stay degraded-only.

Options:

  • (a) No. The route is marked degraded, dependents stop selecting it (Q2b), and challenges back off. Restart happens only on workload death, as today.
  • (b) Yes, after N consecutive failures, under the continuity backoff.

Recommendation: (a) for this campaign. A process that answers 503 while alive is a health-policy question, and making it restart authority mixes observation back into actuation.

Depends on it: B2.

Q6. Landing the CultLib Idunn TS runtime on main (F15).

RULED (a), with one Soul pass on the ten weekend commits before the merge by the operator, 2026-09-29.

Options:

  • (a) Merge codex/fix-node24-ajv-esm into CultLib main as-is, then cut C1 on main.
  • (b) Cut C1 on the branch and merge once.
  • (c) Rewrite the package on main.

Recommendation: (a). It makes the shared substrate honest before changing it. This is a CultLib foundation merge, so the operator should rule on it.

Depends on it: C1 and the StreamPixels gitlink in C2.

Q-B2-1. Does the continuity restart log move into TargetSupervision too?

RULED (a) by the operator, 2026-09-30.

Options:

  • (a) Move it. ContinuityBackoff's own doc (:907-908) already says it "belongs to the target, not the generation". On the generation it survives only through the copy in from_transaction (:2074-2076). Moving it deletes that carry and key 20. The mint CAS pins the target record instead of racing the generation, which is the F3 shape. One record holds both of a target's meters.
  • (b) Keep it on the generation, reshaped to the sliding log in place. The route meter still needs TargetSupervision for first deploys, so the meters split across two records and the carry stays.

Outcome: the continuity restart log lives in TargetSupervision; the restart ceiling reads the target's own log; the carry compensator in from_transaction is deleted.

Depends on it: B2 (see the B2 rework map in section 4).

Q-B2-2. When the route ceiling refuses a first or forward deploy before the fence, does idunn up fail or wait?

RULED: fail at once, by the operator, 2026-09-30.

Options:

  • (a) Fail. The deploy is aborted pre-fence with "route actuation ceiling reached for ; reopens at
  • (b) Wait. A gate wait until reopens_at. The transaction owns the target for up to an hour, and nothing ends that wait before B5's deadlines.

Outcome: a deploy refused by the route/actuation ceiling before the fence fails immediately, with the reopen time in the error.

Depends on it: B2.

Q-R1. Does a routed target answer the stable-route challenge whatever its readiness class?

RULED (a) by the operator, 2026-09-30. The question's first framing was confusing. The cause is that Heimdall has no responder, not its readiness class.

Mechanism (read at 583b2a7): readiness class decides Warming, Ready and whether Routing reads Odin. It never decides whether the stable route is challenged. Routing installs and then requires prove_stable_route for every target with expected.route (:6147-6190), Commit proves it again (:6272-6290), and supervision challenges every routed admitted generation (:5171-5270). Heimdall's recipe (d5acc94) declares odin.verse-rendezvous (Odin-correlated) and is routed, but never answers a route challenge. Heimdall is stateful, so a failed proof leaves the candidate route installed for fail-closed retry (:6171-6175), which past the fence is resumable forever (F17) until B5. Its first idunn up would hold the target with its lease granted, and the failure would surface at the most expensive point.

Options:

  • (a) Yes. The route declaration is the challenge declaration. That is today's rule, made legible and checked before the fence (cut R1). Heimdall gains a responder in owning-repo cut H1, the same shape as StreamPixels C2: bump vendor/CultLib to at least C1 (30ee8b9), answer POST /cultnet/snapshot through the signer's answerRouteObservation, and make reportHealth its single health owner. Idunn gains no schema change, and every routed provider keeps a signed route proof, which Q2(b)/B4 currency needs.
  • (b) No, the class decides. An Odin-correlated routed target is admitted by membership only. That needs a new RoutingEvidence variant (a transaction and generation schema bump), class branches at Routing, Commit and supervision, and loses the proof that the admitted process answers through the stable listener. It also applies to streampixels-service, whose provider currency would then come from Odin, which is the C2 run 2 failure made permanent and contradicts Q2(b).

Outcome: every routed target answers the stable-route challenge regardless of readiness class. R1 checks it before the fence. H1 (Heimdall repo, needs CultLib C1) gives Heimdall a responder.

Depends on it: R1, H1, and Heimdall's first deploy.

Q-B4. For an admitted, running provider, what makes it current enough to satisfy a dependent's deploy?

RULED in part by the operator, 2026-09-30: Idunn's own proof. One clause is held open.

Options:

  • (a) A live readiness proof from the provider's readiness authority. Odin-correlated providers need a current Ready correlation from Odin, so with Odin down or confused no dependent deploy is possible and C2 run 2 fails by design.
  • (b) Idunn's own current observation, whatever the class.
  • (c) Admission plus workload alive, for every provider, with no current readiness evidence.

Ruled, from (b):

  • A routed provider is current by Idunn's route proof, and not degraded (B2's Q5(a) state). This is Q2(b) as ruled, confirmed to override class.
  • An unrouted provider is current by its admission plus a live workload observation of the admitted incarnation.
  • Capabilities come from the signed presence in the route proof (routed) or the admitted Ready receipt (unrouted).

Held clause, STRUCK 2026-09-30 (operator, in Q-O1): "a current Odin non-Ready word still excludes a provider" is rejected. B4 becomes B4′ (section 4, Odin-authority cuts). The original held text follows as history. The recommendation's clause that "a current Odin non-Ready word still excludes a provider" (a stale one would not). The operator: "the last few rulings on Odin are making my authority sense tingle, it feels like Odin is doing lifecycle work when that's very much Idunn's wheelhouse." That clause stays open pending the Odin-lifecycle authority audit, a separate Imagination pass. Until it is ruled, B4 carries no Odin-veto rule either way.

Depends on it: B4′, no longer blocked (the audit landed as section 5).

Q-O1. Who proves readiness for a target that declares the Odin discovery dependency?

RULED (a) by the operator, 2026-09-30: Idunn proves. Idunn's own route challenge always proves readiness. A discovery declaration (odin.verse-rendezvous) never picks a voucher. This re-opens and supersedes Q1 (b); the Q1 text above is history, not live design. Readiness classes disappear at A1 + A3.

Context (audit, section 5): Q1 (b) turned "declares shared-infrastructure odin.verse-rendezvous" into "Odin vouches for readiness", so a discovery declaration selected a lifecycle authority. That reached daemon survival: a continuity restart of ghostlight or streampixels-service waits in Warming with no end until Odin speaks (I2), and Idunn's own boot re-proves every stored Odin receipt against Odin's key (I12). F:\Projects\CLAUDE.md forbids that coupling. Every Odin-correlated target except raven-muninn is routed, so Idunn already owns the channel it needs.

Options:

  • (a) Idunn's own challenge, always. The declaration stays a discovery and graph fact. A1, then A3. Kills I2's survival gap, strikes the Q-B4 clause, supersedes R1.
  • (b) Keep Q1 (b). Keeps I1-I14 and O1-O3; continuity of ghostlight and streampixels-service needs Odin alive and correct; Idunn's boot needs Odin's anchor and key.

Outcome: (a). Depends on it: A1, A3, B4′, and the demotion of ReadinessClass.

Q-O2. How does Idunn prove an unrouted target (today only raven-muninn)?

RULED (a) by the operator, 2026-09-30: a private challenge endpoint. The binding declares a challenge endpoint without a stable route, and Idunn challenges it directly. Cut A2, plus a Muninn responder (M1).

Options:

  • (a) A binding-declared private challenge endpoint (A2 + M1).
  • (b) Workload-alive only: no readiness proof; dependents can never see degradation, which contradicts Q5 (a).
  • (c) Keep Odin correlation for unrouted targets only: A3 is then impossible.

Outcome: (a). raven-muninn is held today and has no continuity restarts, so there is no live regression while it waits. Depends on it: A2, M1, and raven-muninn leaving the held state.

Q-O3. The three admitted Odin-voucher generations once A1 installs. OPEN. NOT YET ASKED. Needed before A1 installs.

Admitted generations on Odin receipts that A1 will hold as WrongVoucher: odin, streampixels-service, ghostlight (streampixels-web is Odin-free already).

Options:

  • (a) Redeploy each immediately after install, in the order odin -> streampixels-service -> ghostlight -> web, with the operator present for the window. No new code (the existing rule: "reported and held, never repaired"). Each is continuity-less for its redeploy window; held generations mint no continuity (B3 F1). Audit recommendation.
  • (b) A one-shot relabel: supervision replaces the Odin receipts with a fresh stable-route proof once. No continuity gap, but it adds the repair path the hold rule was written to forbid.

Q-O4. Does shared-infrastructure odin.verse-rendezvous still gate a dependent's plan compile?

RULED (b) by the operator, 2026-09-30: the Odin dependency does not gate a dependent's compile; declare it optional. It is a discovery need that never gates (I11). The service must tolerate Odin's absence at runtime and publish when Odin returns.

Options:

  • (a) Yes. Odin is an ordinary provider judged by Idunn's own proof of Odin (B4′); an Odin outage blocks deploys, not continuity, of Verse-advertising services.
  • (b) No. Services declare it optional.

Outcome: (b). Depends on it: the ghostlight, streampixels-service and raven-muninn recipes (flipped inside A1's window), and the I11 graph gate.

Q-O5. A live process whose challenge answer does not authenticate as the admitted incarnation.

RULED (a) by the operator, 2026-09-30: restart on bad auth. Supervision restarts a live process whose challenge answer does not authenticate as the admitted incarnation (bad signature, wrong activation or instance, lease not held), under the continuity restart meter in TargetSupervision. Silent answers stay degraded-only. This amends Q5 (a) narrowly; it applies to every routed target, not only Odin.

Rationale (audit): a refused-as-unauthenticatable answer is Idunn's own evidence that what runs is not what it admitted. That is survival authority, not health policy. It lets Odin's temporary fatal rule (section 4) be deleted. Rejected: (b) Q5 (a) unchanged, with the temporary rule kept permanently.

Depends on it: a supervision change on B2's meter (no cut drawn yet; see the Cut status), and the deletion of Odin's temporary rule.


4. Draft cut list

Order is deletions first, then one foundation, then behaviour cuts, then the owning-repo cuts. Each cut is one Hands brief and one Soul pass.

Subtraction

S1 — Observation stops actuating (Idunn).

  • Delete the stale-observation → repair-intent → restore_admitted_membership path from supervise_admitted_route.
  • Supervision becomes challenge-only: an exact fragment plus a stale observation yields exactly one prove_stable_route_against, with no write, no nginx -t, no private-mount systemd-run, no ufw and no reload.
  • restore_admitted_membership runs only when the fragment on disk differs from the admitted membership. It stops re-admitting ufw when the fragment is unchanged.
  • The route_repair_started_at field stays readable but its decision power is deleted. F0 retires it.
  • Negative checks:
    • A failed proof with an exact fragment performs zero actuations.
    • A byte-identical fragment never reloads.
    • Drifted bytes reload once.
  • It touches supervise_admitted_route and NginxRouteDriver. It does not touch the source-freeze and digest regions that cut1-fix5 rewrites. The merge hazard is limited to the drivers.rs test module and imports.

S2 — Delete the CLI post-fencing cancel (Idunn), after B5. Remove cancel_is_safe_for_live_stateless and the live-transaction branch of cancel (8ae00a1). Pre-freeze command cancellation stays. The Q4 expiry request replaces the rest.

S3 — The web drops Odin (StreamPixels), after C1. Remove the odin.verse-rendezvous dependency and STREAMPIXELS_ODIN_CULTMESH_RUDP from deployment/idunn/web.toml. Remove the web branch's Odin publisher, publishInitialWarming, and the heartbeat publication from runtime-presence.mjs. The service keeps its own under Q1(b).

Foundation

F0 — Typed readiness, deadlines, and actuation state (Idunn schema: one migration).

  • Bump idunn.admitted_generation to v3 and idunn.deployment_transaction to v4, carrying:
    • the readiness-evidence enum;
    • route supervision state, replacing route_repair_started_at;
    • continuity backoff;
    • phase deadlines;
    • lease adoption evidence;
    • terminal recovery variants.
  • The readiness class is derived from Expected (Q1).
  • Migrate every existing v2 generation as OdinCorrelated with its current receipts, so behaviour does not change at this cut.
  • Deadline durations become optional binding fields with Idunn defaults, and are frozen into the compiled plan.
  • No decision reads the new fields yet, apart from S1's retirement of the old one.
  • Tests: migration round-trip over a copy of the live store's shape. A v2 record must decode to the same decisions.

Behaviour

B1 — A continuity abort cleans its own projection, and a148802 is reverted (Idunn).

  • The pre- and post-fencing abort intents set topology_reconciliation = Pending for Continuity.
  • The resolution demotes the shared key to Expected-only with the transaction's own activation (exact), and never withdraws the Expected.
  • Delete demote_current_activation_to_expected_only. Rollback demotes the admitted generation's exact activation.
  • Negative check: a failed continuity followed by a deploy that fences and aborts resolves with the exact admitted activation, and no path adopts an activation the transaction did not issue.
  • F0 is not required. This cut could land before F0 if Self wants the drift closed first.

B1 status, 2026-09-29 (Self). Landed on idunn/route-b1 (87fa57d revert of a148802, 4427769, 4a9fe87); 177 tests pass. Soul (Opus): do not close. The rule held (one rule, one resolution, exact activation, the Expected never withdrawn, foreign activations refused, the revert exact). The transition failed:

  • High, CONFIRMED: a continuity abort persisted under the old rule (Skipped although an activation was issued) fails the new validation (control_plane.rs:1031-1044). ControlSnapshot::read fails the whole store on one record, so a resident record stops Idunn from booting, for every target.
  • Medium, CONFIRMED: supervision's pre-restart demotion failure is only logged (:3557-3576). A continuity failing between prepare_activation and publish_observed_activation then wedges its abort on "substituted" every tick. Drift left by a pre-B1 failed continuity wedges a later deploy abort the same way.
  • Low: docs/deployment-authority.md says the projection "never drifts".

Self's ruling for the B1 fix batch, which runs on top of F0 because F0 owns the legacy lift:

  1. (superseded below) The legacy-transaction lift maps a pre-B1 continuity abort that issued an activation to Pending. The new single resolution then demotes its own activation, which also cleans the residue that record left behind.
  2. Supervision owns the Expected-only precondition. It does not mint a continuity while its pre-restart demotion fails; the failure is recorded, not merely logged.
  3. A one-time boot reconciliation demotes a projected activation only when its issuing transaction is a failed transaction in history (exact identity). It never adopts.
  4. Engine-layer tests for both abort paths, using Soul's EngineFixture probe shape (candidate cleanup already Complete, so no systemctl runs). cargo-mutants found 6 Engine-path mutants missed.
  5. Correct the doc's "never drifts". Before any deploy, check the live host for resident continuity abort records. That is a read of control.cc, and the operator's to authorise.

B1 fix batch 1, Soul pass, 2026-09-29: do not close. Self's corrected ruling replaces item 1 above. Hands implemented item 1 by reopening terminal pre-B1 records at Starting. Soul confirmed the cost:

  • If the target also holds a live transaction, or a second resident pre-B1 abort, the reopened record fails ControlSnapshot::read and Idunn does not boot.
  • The migration has already written the record as v4, so the old binary cannot roll back.
  • The reopen is a second owner for residue that boot reconciliation already cleans. The defect was in Self's ruling: it set a state on a record without saying what happens to terminal records.

Corrected ruling:

  • Nothing is reopened.
  • A terminal pre-B1 abort keeps its phase. Validation accepts its shape under a typed legacy marker that only the lift sets.
  • Boot reconciliation is the single owner of legacy residue.
  • In-flight pre-B1 aborts may lift to Pending only if they cannot collide with another live transaction.

Also fixed in batch 2:

  • A failed pre-fence abort goes to record_resumable_error, never to begin_post_fencing_abort. Before this, one wedged abort failed every scheduler tick for every target.
  • Errors are isolated per transaction, including archive_terminal_transaction.
  • serve's boot wiring is pinned by a test.

Moved to B2: the continuity restart ceiling reads only the target's restart log in TargetSupervision, never history.cc. Otherwise an unreadable history file stops crash recovery for every target. Deferral and backoff show in idunn status.

B2 — Bounded, backed-off route and unit repair (Idunn). READY FOR HANDS; rework map below.

Widened 2026-09-29 by Self, from Soul's S1 pass. S1 closed the healthy-target storm, and Soul found two storm paths S1 does not claim. Both are B2's:

  • A failing reload deletes the fragment (restore_admitted_membership, drivers.rs:5472-5478). The next tick reads the file as missing, treats that as drift, and restores again: one systemd-run, ufw, nginx -t and reload every 500 ms, unbounded. This was probed over 4 ticks. A broken global nginx config does the same through the private-mount unit.
  • NginxRouteDriver::install (drivers.rs:5402) runs ufw, nginx -t and reload unconditionally. The Routing phase re-runs it on every resume, and a post-fence proof failure resumes forever (F17), so a candidate failing its proof reloads every tick. A stateless candidate reloads twice (install, then rollback). The post-fence abort's withdraw_candidate_membership then calls restore, which does the same.

So B2's actuation ceiling covers install, restore and restore_admitted_membership, not only supervision. B2 also adds the seam Soul specified: the route actuator program paths and preflight_root on RuntimeOptions, defaulting to today's paths and used at every NginxRouteDriver::new site, plus a routed Stored<AdmittedGeneration> fixture beside EngineFixture. Deleting supervise_admitted_route's body currently survives every test (cargo-mutants), and B2's reload-count timeline test needs the same seam. A failed proof marks the route degraded (Q5 a). Today nothing is written and the next tick re-challenges with no backoff.

Rework map, 2026-09-30 (Imagination pass, Opus). Every file:line in this block is against Idunn 583b2a7 (the code under main 94c7691, installed on Yggdrasil). The salvage source is origin/idunn/route-b2 (97f1ad2, +1243/-200; merged with B1 at 1786ddf). The rework is a fresh branch from 583b2a7, not a rebase of 1786ddf. Rulings Q-B2-1 (a) and Q-B2-2 (fail at once) are in section 3.

B2 rework, A. What B3 superseded, and what survives from 97f1ad2

Delete; do not port. B3 or this rework owns each of these now:

  • routed_generation, hanging_up_listener, RoutedFixture, routed_transaction, store_seeded, the seeded_record split, route_proof_evidence() hand evidence, and DeadWorkload.
    • B3's RoutedWorld (control_plane.rs:14255-14660) already builds a real route-proof admission, with a RuntimeStub at both endpoints and a hang_up switch (:14068).
    • SwitchWorkload::kill (:11932-11945) is the dead workload.
    • These are why B2 "does not compile on B3, and two tests fail because the fixture target is route-proof".
  • Engine::update_admitted_generation, the re-read-then-CAS helper. It existed because B2 charged the ceiling to the generation, so every other writer had to re-read to avoid losing the charge. The meter moves off the generation (section B), so writers CAS on the envelope they read. Tests use the existing edit_incumbent (:12065).
  • ActuationWindow::charge: a fixed window anchored at the first charge. That is Soul's F4.
  • The first-deploy exemption in charge_route_actuation (let Some(current) = admitted_for else return Ok(())). The 2026-09-30 ruling removes it.
  • ContinuityBackoff.deferral_reason added as a v3 field. That is Soul's F1. The reason moves to the new record (section B).

Port, reshaped as sections B-E say:

  • In drivers.rs:
    • RouteActuators and its Default;
    • NginxRouteDriver::with_actuators, with new delegating to it;
    • the gate trait threaded through every mutating method;
    • the Unmetered test gate;
    • a_refusing_gate_stops_every_route_actuation_before_it_starts, split in two (section E).
  • In control_plane.rs:
    • RuntimeOptions.route_actuators;
    • Engine::route_driver, used at every construction site;
    • EngineRouteGate;
    • the RouteStubs fixture and EngineFixture::routed;
    • RouteSupervisionState::{is_waiting, record_failed_challenge, record_proved_challenge};
    • the supervise_admitted_route restructure, where a failed repair or proof records state;
    • render_supervision plus the status loop;
    • the continuity ceiling replacing the history count;
    • the restart counted in the minting CAS;
    • the tests continuity_restarts_are_backed_off_and_bounded_per_target, status_renders_…, a_deployment_yields_to_a_restart_only_while_the_target_has_restarts_left, and the storm tests, rebuilt on RoutedWorld.
  • Constants, kept from B2 because Soul did not dispute them:
    • CONTINUITY_RESTART_ATTEMPTS = 6 per CONTINUITY_RESTART_WINDOW_MILLIS = 3_600_000;
    • CONTINUITY_RESTART_BACKOFF_MILLIS = 5_000, doubling;
    • ROUTE_ACTUATION_CEILING = 12 per ROUTE_ACTUATION_WINDOW_MILLIS = 3_600_000;
    • ROUTE_CHALLENGE_BACKOFF_CAP_MILLIS = 600_000.

B2 rework, B. Owner map (ownership changes)

Route actuation rate.

  • Owner: Engine::charge_route_actuation(target, RouteActuation), over a new per-target record, TargetSupervision.route_actuations.
  • Inputs: the target's record from a fresh control snapshot (absent means empty), now, and the kind: Forward or Survival.
  • Output: Ok(()), or the typed RouteActuationRefused { target, used, reopens_at }.
    • Only Forward can be refused.
    • Survival is always recorded and always admitted (ruling: never refuse a rollback).
  • Demoted:
    • RouteSupervisionState.actuations is dead, deleted in v4.
    • RouteSupervisionState::for_new_incarnation (:885-893) is dead. A new incarnation's route state is default().
  • Forbidden writers:
    • the driver, which only asks;
    • AdmittedGeneration::from_transaction, which no longer carries a meter;
    • the commit CAS.
  • Shared paths:
    • Every NginxRouteDriver is built by Engine::route_driver. The seven ::new sites are :5211, :5701, :6151, :6280, :6985, :7323 and :7652.
    • Every mutating driver method takes &dyn RouteActuationGate, so none can be called without the meter. The compiler enforces it.

Continuity restart pacing (Q-B2-1 ruled (a)).

  • Owner: the mint in supervise_one_admitted_generation, over TargetSupervision.continuity_restarts and continuity_deferred_until.
  • Forbidden writers and inputs:
    • history.cc: the history_for_decision call at :5003 goes;
    • the generation id: the count is no longer per generation;
    • from_transaction's backoff carry at :2074-2076.
  • Demoted: AdmittedGeneration.continuity_backoff (key 20) is dead, deleted in v4.

Route challenge pacing stays per incarnation, on the generation.

  • Owner: supervise_admitted_route, over RouteSupervisionState without actuations.
  • route_repair_started_at_unix_millis (key 18) is dead, deleted in v4.
    • Its last readers are the validate block at :2139-2145 and the unrouted guard at :5173-5176.
    • Its last writer is the clear at :5263.

One generation write per supervision iteration (structural fix for F3). Any write to a target's generation or to its TargetSupervision ends that target's iteration: progressed = true; continue.

  • refresh_admitted_topology already follows this rule (:4827-4835).
  • The route challenge is the violator. Today a failure propagates as Err after B2's state write, and the restart path then CASes a stale envelope.

New persistent type, TargetSupervision (idunn.target_supervision, schema v1, in control.cc, keyed by target).

  • Owner: the meter primitives above.
  • Live consumers:
    • the route gate (every install, preflight, rollback, withdraw and repair);
    • the continuity mint;
    • status.
  • Invariant: a target's meters outlive every generation and exist before the first one.
  • Why no existing owner can serve:
    • The generation does not exist for a first deploy (the ruling).
    • The transaction dies with each attempt.
    • Brakes are operator-signed and are not Idunn's to write.
  • What it replaces: two carry compensators, for_new_incarnation and the backoff copy in from_transaction, plus a history scan.

B2 rework, C. Per file

The branch is fresh from 583b2a7.

Deletes first.

control_plane.rs:

  • :63: CONTINUITY_RESTART_ATTEMPTS: usize = 3. It is replaced by the constants in section A.
  • :876-893: ActuationWindow and for_new_incarnation.
  • :906-926: ContinuityBackoff. Q-B2-1 (a) moves it into TargetSupervision.
  • :1986-1988: key 18.
  • :1992-1993: key 20.
  • :2043-2047 and :2074-2076: the carries.
  • :2139-2145: the key-18 validation.
  • :4974-5020: the history-counted is_refused_restart and the history_for_decision read.
  • :5062-5071: the deferral check on the generation.
  • :5173-5176: the key-18 guard. Keep the SkippedUnrouted half.
  • :5263: the key-18 clear.

Tests:

  • :9755-9801, continuity_gives_up_on_a_release_that_will_not_start. It tests a copy of the production closure, so it proves nothing.
  • :10564, :11528-11542: the F0 assertions on ActuationWindow and attempts. Rewrite them against TargetSupervision::validate.
  • :14622-14645, RoutedWorld::promote_by_hand, and its 9 callers (:14651, :14691, :14722, :14767, :14927, :14949, :15041, :15299, :15433). They become routed.step(): the stub actuators let Routing run for real, so route-proof Routing gets tested for the first time.

Schema: generation v4, one bump.

  • ADMITTED_GENERATION_SCHEMA (:55) becomes "idunn.admitted_generation.v4". The macro at :1939 changes to match.
  • :56: add ADMITTED_GENERATION_SCHEMA_V3.
  • The v4 layout:
    • keys 0-17 are unchanged;
    • 18 is a gap;
    • 19 is Option<RouteSupervisionState> without actuations;
    • 20 is a gap;
    • 21 is last_error: Option<String>, the dead-hold ruling's field. It is written by D1, and B2 carries the slot so D1 needs no second bump.
  • The cultcache derive encodes slots positionally, so a gap is a nil and any added slot changes the array. That is why F1's unbumped field broke the canonical re-encode (decode_record, :2811-2821), and why every change here rides one bump.
  • LegacyAdmittedGenerationV3: move today's AdmittedGeneration (:1936-1995), RouteSupervisionState and ActuationWindow, and ContinuityBackoff verbatim under Legacy* names beside LegacyAdmittedGeneration (:2967-3049).
    • Its into_current() drops key 18, actuations, window_started_at, attempts and next_restart_at.
    • It refuses a v3 record whose actuations.count != 0 or attempts != 0.
    • Why that is lossless (read): no released binary ever wrote those fields. On 583b2a7 the only non-test writes are the carry at :890, which carries a default, and the ≤30 s deferral next_restart_at at :5154. B2 was never installed.
    • So the lift creates no TargetSupervision. Every target starts with an absent record, and the refusal makes any future contradiction loud instead of silently dropped.
  • The v2 lift (:3016-3048) targets v4 directly: the same body without the retired fields. Keep it; section H explains why this is not a question.
  • read_generation_record (:3102-3121): add a v3 arm.
  • The migration filter at :3147-3148 adds ADMITTED_GENERATION_SCHEMA_V3.

New record, TargetSupervision, beside AdmittedGeneration:

struct TargetSupervision {           // idunn.target_supervision / .v1, key = target
    #[cultcache(key = 0)] schema_version: String,
    #[cultcache(key = 1)] target: String,
    /// Newest ≤ ROUTE_ACTUATION_CEILING actuation times, ascending.
    #[cultcache(key = 2)] route_actuations: Vec<u64>,
    /// Newest ≤ CONTINUITY_RESTART_ATTEMPTS restart times, ascending.   (Q-B2-1 (a))
    #[cultcache(key = 3)] continuity_restarts: Vec<u64>,
    #[cultcache(key = 4)] continuity_deferred_until: Option<u64>,
    #[cultcache(key = 5)] continuity_deferral_reason: Option<String>,
}

The record threads through several places:

  • ControlSnapshot (:2564-2568) gains targets: Vec<Stored<TargetSupervision>>.
  • The read match at :2580-2610 gains its arm. Without it, the match's "foreign document" bail at :2609 refuses the store.
  • validate_relations checks unique targets.
  • target_supervision_envelope goes beside admitted_envelope (:3198).

Meters. A sliding log, not a window (fixes F4):

  • charge(log, now, window, limit, kind):
    1. Clamp any entry greater than now to now (fixes F5; see below).
    2. Count the entries within window.
    3. For Forward, if the count is at least limit, refuse with reopens_at = log[len - limit] + window.
    4. Otherwise push now and keep only the newest limit entries.
  • Keeping the newest limit entries is exact for "at most limit Forward in any window", even after Survival pushes past the limit.
  • The restart log uses the same primitive. The next restart is due at last + BACKOFF << (n - 1), where n is the number of entries in the window.

Clock steps. Fixes F5, and collapses one helper.

  • is_waiting(now, not_before) (:3351-3353) becomes is_waiting(now, not_before, longest_wait) = now < not_before && not_before - now <= longest_wait.
    • A due time further ahead than any wait this owner can set means the clock stepped back, so the attempt is due.
  • Callers:
    • resume backoff at :4537, with RESUME_BACKOFF_CEILING_MILLIS;
    • the route challenge, with ROUTE_CHALLENGE_BACKOFF_CAP_MILLIS.max(max_age);
    • the restart wait, with BACKOFF << (ATTEMPTS - 1);
    • the deferral, with CONTINUITY_DEFERRAL_MILLIS.
  • The log clamp bounds a backwards step to one window, never the length of the step.

drivers.rs.

  • Port RouteActuators from 97f1ad2 at :5049-5068.
  • The trait becomes RouteActuationGate::admit(&self, kind: RouteActuation) -> Result<()>, with pub enum RouteActuation { Forward, Survival }.
  • Gate the methods:
Method Kind
preflight (:5293), before validate_candidate_in_private_mount at :5315 Forward (F7)
install (:5338), before the write at :5360 Forward
restore (:5268) and fail_after_rollback (:5279) Survival
withdraw_candidate_membership (:5409) Survival
rollback (:5555) Survival
restore_admitted_membership (:5420), after the exact-bytes return at :5430 Survival
  • install's own rollback, through fail_after_rollback, therefore charges a Survival even when it follows a Forward.

control_plane.rs Engine.

  • RuntimeOptions (:2160, :2202): add route_actuators.
  • Add route_driver and route_gate as in 97f1ad2.
  • charge_route_actuation:
    • reads a fresh snapshot;
    • CASes the target's record, with current: None when absent (the first deploy);
    • is typed as described above.
  • A refused Forward is an error of its phase.
    • Warming's preflight (:5690-5711) comes before the fence, so the refusal becomes a pre-fence abort (:4587-4593) whose error names reopens_at (Q-B2-2 ruled: fail at once).
    • Routing's install comes after the fence, so it stays resumable. The resume backoff spaces it, and no actuation runs.
    • Until B5 lands, a post-fence candidate whose install keeps being refused waits for the window. That wait is bounded by the window, and it runs no programs.

supervise_one_admitted_generation (:4787-5145), in order:

  1. Hold (:4960-4973, unchanged).
  2. Blocker yield (:5025-5043): refused_restarts < ATTEMPTS becomes !restarts_exhausted(now).
  3. Exhausted: eprintln once through a ReportOnce keyed continuity:<target>, then continue.
  4. Waiting or deferred: continue.
  5. Demotion (:5072-5098). On failure, write continuity_deferred_until and the reason to TargetSupervision, then progressed = true; continue.
  6. Lifecycle brake (:5105, unchanged).
  7. Mint. The CAS at :5114-5142 gains the TargetSupervision expected envelope, None when absent, and its next value with now charged to the restart log and the deferral cleared.

A brake-parked target spends no attempt.

supervise_admitted_route (:5171-5270).

  • At the top: if route_supervision.is_waiting(now, cap), return Ok(false).
  • Port B2's challenge closure: the Survival-gated restore, prove, and re-observe.
  • On Err:
    • write record_failed_challenge to the generation, CAS on current.envelope;
    • print once through a ReportOnce keyed route:<target>;
    • return Ok(true). This is the F3 fix.
  • On Ok: write the receipt and record_proved_challenge, return Ok(true).
  • If the Survival repair itself charged TargetSupervision, that is a second record, not the generation, so the generation CAS still holds. The iteration ends either way.

status (:3668-3756).

  • After the command loop, print one block per target: the union of admitted generations and TargetSupervision records.
  • Port render_supervision, reading the restart and route logs from the target record:
    • route actuations N/12 in window, reopens-at …;
    • continuity restarts N/6 … next-restart-at … deferred: <reason>.
  • A metered target with no generation prints its meters alone. That is the first-deploy case.

B2 rework, D. B2 verification

Each ruling or finding is pinned by a test that fails under its own mutation. All tests are Engine-level on RoutedWorld with RouteStubs, #[cfg(unix)], except where marked unit.

Ruling / finding Test Mutation it must kill
Ruling: never refuse a rollback a_full_ledger_still_restores_the_admitted_and_incumbent_route. The ledger holds 12 entries inside the window. (i) A post-fence abort's withdraw runs systemctl reload once and pushes an entry. (ii) Supervision repairs a drifted admitted fragment the same way. (iii) A Routing proof failure's rollback does too. Survival treated like Forward (refused), which drops the reload; Survival not recorded, which drops the entry
Ruling: always counted the same test, asserting the ledger's newest entry is now after each Survival skipping the push for Survival
Ruling: first deploys metered a_first_routed_deploy_is_metered_and_refused_at_the_ceiling. RoutedWorld seeds with no incumbent (:14352). (i) run_to_routing then one step creates TargetSupervision with 1 entry. (ii) A world whose record is pre-filled to 12: install fails with RouteActuationRefused, and the stub log has no ufw, nginx or systemctl. the B2 early return "no admitted generation means unmetered"
F7 a_refused_preflight_starts_no_private_mount_unit, a full ledger: the Warming preflight runs no systemd-run and the transaction pre-fence aborts with the reopen time preflight's gate call deleted
F4 rolling unit, the_actuation_log_slides: 12 charges at t, t+1…t+11. At t+window only one slot reopens (the 13th charge succeeds, the 14th is refused until t+1+window). fixed window anchored at the first charge
F5 unit, a_clock_stepped_back_stalls_nothing_past_one_window: log entries and due times at T, with now = T − 1 day. The Forward reopen is ≤ now + window; the route challenge, restart and resume waits read as due. the clamp removed; the longest_wait guard removed
F3 a_failed_challenge_and_a_dead_unit_in_one_tick_do_not_lose_a_cas. A drifted fragment, a hang_up stub and a killed SwitchWorkload. Tick 1 returns Ok and records the failure; tick 2 mints the restart. a route failure returns Err, or falls through without continue: the mint CAS loses and the tick errors
F6 a_proved_challenge_clears_the_degradation: hang_up on, one tick (degraded, failures 1); hang_up off, wait made due, one tick (failures 0, next None, degraded None) record_proved_challenge call deleted
F1 a_v3_generation_lifts_to_v4_once_and_reencodes_canonically, over new golden fixtures generation-v3-odin.hex and generation-v3-route-proof.hex, cut with the 583b2a7 encoder (add them to the fixture README). The migration counts 1, then 0. After it, decode_record accepts the bytes, and the decisions are equal apart from the retired fields. A v3 fixture edited to attempts = 2 is refused. the v3 schema dropped from the migration filter; the refusal removed
Restart ceiling reads no history rename unreadable_history_stops_continuity… (:13516) as B2 did: a corrupt history still restarts, and the target's own log exhausts it re-adding history_for_decision
Backoff and window port continuity_restarts_are_backed_off_and_bounded_per_target on SwitchWorkload halving the wait; resetting the log on a new generation
Storm (a) port a_failing_reload_that_deletes_the_fragment_backs_the_route_off: 200 ticks give one reload the is_waiting gate deleted
Storm (b) port a_candidate_whose_route_proof_fails_reloads_at_most_the_ceiling: installs and their Survival rollbacks share the 12-per-hour route ledger, so a stateless candidate whose proof keeps failing gets 6 install+rollback pairs per hour; only Forward is refused at the ceiling, and Survival is always admitted (counted when the ledger can record it; ruled 2026-09-30 after B2 Soul) the install gate deleted
Driver gates split B2's refusing-gate test in two: Forward refused runs no program; Survival under a gate that "refuses" Forward still runs a gate call moved after the write
  • Run cargo mutants --in-diff on the cut. The target is 0 missed in the meters, the migration and the supervision edits.
  • a_failing_step_is_retried_after_a_backoff_not_every_tick is flaky under a full parallel run on the base too. Soul should not charge it to this cut.
  • The live timeline probe, read-only, before and after install:
    • journalctl -u nginx --since -1h | grep -c 'Reloaded nginx' stays at the C2 baseline of about 0 per hour while steady;
    • idunn status shows every routed target with route actuations 0/12 and a healthy route.

B2 rework, E. Live-deploy relevance (read from migration code, not from the store)

What is live now (ship log plus code).

  • control.cc generations are v3: F0's lift (:3131-3181) ran at the B3 install and migrated 5 records.
  • Transactions are v4.
  • history.cc holds v3 and v4 transactions and is never migrated (:3388-3391).
  • B2 changes no transaction schema.

What the B2 install does.

  • At boot, the migration rewrites every v3 generation as v4, each by CAS against its exact envelope. That is the same machinery F0 used.
  • It writes no TargetSupervision: the lift is lossless for the reasons in section C.
  • The first route actuation after install, a deploy or a repair, creates each target's record.

It is irreversible.

  • The pre-B2 binary refuses a v4 generation (:3120).
  • It also refuses a TargetSupervision envelope: ControlSnapshot::read bails "foreign document" at :2609.
  • Binary rollback therefore means restoring the backup.
  • Ship steps: back up control.cc and history.cc, install, then check that idunn status renders every target.

A lift refusal fails boot for every target. It cannot happen from any released binary. If it does, the backup plus the pre-B2 binary is the recovery, and the error names the record.

Raven.

  • idunn-host decodes plans, not generations, so B2 needs no actuator rebuild.
  • W1 is still the check that the actuator builds at the sha that ships.

B2 rework, F. Subtraction estimate (B2)

Production code in control_plane.rs and drivers.rs:

  • Removed:
    • about 45 lines of history ceiling;
    • about 40 lines of ActuationWindow, carry and backoff;
    • about 20 lines of key-18 and key-20 plumbing.
  • Added:
    • about 90 lines for TargetSupervision, its snapshot arm and its validation;
    • about 60 lines for the meter primitive and the clock guard;
    • about 60 lines for the v3 legacy layout, mostly moved rather than written;
    • about 60 lines for the gate plumbing and RouteActuators;
    • about 50 lines for status.
  • Net production: about +250.

Tests:

  • Removed: about 46 lines of the tautology test and about 24 lines of promote_by_hand.
  • Net tests: about +350. The 9 hand promotions become real Routing steps.

Compared with 97f1ad2, which was +1243/-200, about 600 of B2's lines are not ported.

Structural delta:

  • One record type added.
  • Two carry compensators and one history dependency deleted.
  • Two generation keys retired.
  • No targets, crates or dependencies.
  • The build and test matrix is unchanged, apart from W1's check.

Not asked, decided. The v2 generation lift stays, retargeted to v4. Its only consumer is a restore of /root/idunn-store-backup-…-pre-route-b3, and deleting it would force rewriting about 10 tests built on FIXTURE_GENERATION for a saving of about 80 lines. Retire the v2 and v3 generation layouts together in one later cut, once the operator retires the pre-B3 and pre-B2 backups.

D1 — The dead-hold report lives on the generation (Idunn; operator ruling, 2026-09-30). READY FOR HANDS, after B2 (B2 carries key 21 and render_supervision).

Owner: Engine::report_generation_hold(current, detail). It is the only writer of AdmittedGeneration.last_error.

  • It writes only when the text differs.
  • It prints once through the existing ReportOnce map (:3865), keyed by target.
  • It ends the iteration, per the rule in section B.

Writers today, by site.

  • note_fault("holds admitted generation", "generation:<t>") at :4793-4799 hands a generation key to record_last_error (:4472-4495). That function looks it up among transactions, finds nothing, and so the report is stderr only. It also never clears.
  • The dead-hold note at :4965-4969 behaves the same way.
  • Both go. They are replaced by report_generation_hold, with the readiness disagreement, or with "is not running; declare readiness in its recipe and redeploy" when dead.

Clearing (the ruling: replaced, or hold released).

  • Replacement clears by construction. from_transaction sets None, so it needs no code.
  • Release: at the hold check, readiness().is_ok() && last_error.is_some() writes None, calls clear_fault, and continues.
    • This covers an Idunn upgrade that reclassifies a generation.
    • last_error has no other writer: the continuity deferral reason lives in TargetSupervision, and route failures live in RouteSupervisionState.

Status. render_supervision prints held: <last_error>.

Tests.

  • Extend a_held_generation_that_dies_mints_no_continuity_and_leaves_its_target_free (:13842). It currently reads the in-memory fault_reports at :13858-13859; it must read last_error from the stored generation instead: first the held text, then, after SwitchWorkload::kill, the dead text.
  • Add a_released_hold_clears_its_report: seed last_error on a generation whose readiness is OK; one tick clears it, and a second tick writes nothing, with the envelope unchanged.
  • Mutations it must kill:
    • dropping the clear;
    • writing on every tick;
    • routing back to record_last_error.

Size. About +40 production lines and about +60 test lines. It deletes the two generation:-keyed note_fault calls.

Live. No schema change. On install, the first tick writes last_error on any held generation (none per the 2026-09-30 decode, now that raven-muninn is admitted).

T1 — Pin the candidate-cleanup rule by removing its free arguments (Idunn; Soul follow-up on B3). IN PROGRESS on hands/idunn-w1-t1-t2.

The defect.

  • pre_fencing_abort_intent (:7991-8005) and post_fencing_abort_intent (:8007-8039) each call candidate_cleanup_requirement(activation.is_some(), workload.is_some()) (:8041-8050).
  • The unit test (:9806-9820) never covers (false, true), and no test drives either intent with exactly one of the two set.
  • So swapping or constant-folding either argument at either call site survives.

The cut.

  • Delete the helper.
  • Add DeploymentTransaction::candidate_cleanup_owed(&self) -> CleanupEvidence, used by both intents.
  • Rewrite the unit test as a four-row table over the method.
  • Add one test that both intents take the method's answer for (Some, None) and (None, Some).

Mutations it must kill:

  • || becoming &&;
  • either operand becoming false;
  • either intent hardcoding Skipped.

Size. About −10 production lines and about +8 test lines. Not live-relevant.

T2 — The chunk-size line is exactly RFC 9112 (Idunn; Soul follow-up on B3). IN PROGRESS on hands/idunn-w1-t1-t2.

Probe (Yggdrasil, throwaway commit ce98e9b on 583b2a7): the HTTP reader accepts " 3", "3 ", "3 ;x" and "3" alike. The cause is size.trim_matches([' ', '\t']) in drivers.rs:5816-5821.

What RFC 9112 7.1 allows. chunk-size [ chunk-ext ] CRLF, where chunk-ext = *( BWS ";" … ). So whitespace is allowed only before a ;, and never before the digits.

The cut.

  • Split at the first ;.
  • Trim trailing SP and HTAB only when an extension follows.
  • Never trim leading whitespace.
  • parse_digits (:5846-5851) stays the digit authority.

Tests. Add rows to an_http_answer_that_frames_its_body_wrongly_is_refused (:8611): " 3" and "3 " are refused. Add to a_chunked_answer_must_end_its_trailers_and_terminate_each_chunk (:8799) that "3 ;x" and "3\t;x" are taken.

Mutations it must kill:

  • trim both ends, which lets " 3" pass;
  • no trim, which refuses "3 ;x".

Size. About +4 production lines and about +8 test lines. It is live-relevant only as stricter parsing of StreamPixels and Odin answers. Node's http emits bare hex, so there is no expected behaviour change.

Note. Neither T1's nor T2's text is recorded in the map at 94c7691. Both were taken from the brief, and the defects were confirmed from code and the probe.

W1 — Every verification checks the Windows actuator compiles (Idunn map and the eureka stopgap image). IN PROGRESS on hands/idunn-w1-t1-t2.

Probe (Yggdrasil, rust image):

  • rustup target add x86_64-pc-windows-gnu; cargo check --locked --target x86_64-pc-windows-gnu --bin idunn-host.
  • At d32395a (B3 merge, before the fix) it fails with four E0425 errors, "cannot find value FROZEN_SOURCE_SYMLINK_TARGET_LIMIT" (drivers.rs:1461-1477). That is exactly the Raven break.
  • At 583b2a7 it passes in 26 s. No mingw linker is needed, because check does not link, and no dependency compiles C for the target.

The cut. No Idunn code changes.

  1. ~/.claude/skills/eureka/tools/stopgap/rust.Dockerfile gains RUN rustup target add x86_64-pc-windows-gnu, so no job downloads rust-std. Rename the file over itself; the image is re-tagged by the file's hash.
  2. The map's "Build and verification path" makes the check a required step for every Idunn cut, beside cargo test.
    • It must not use -D warnings: the Windows target currently emits 9 warnings (unused cfg-split code).
  3. When the Idunn verify recipe exists (verify campaign), the same step moves into it, and this stopgap line dies with ygg-verify.sh.
  4. The release build stays a documented step, not a verification.
    • gamecult-ops/runbooks/idunn-host-raven.md:23 already names Starfire for cargo build --release --bin idunn-host. Add: "at the exact Idunn sha being installed on Yggdrasil; record the exe sha256 in the ship log".
    • That is Windows-only work. It fits the load budget: one job, no burners.

Honesty limit.

  • W1 catches compile rot, the class that broke Raven.
  • It does not catch link failures or runtime behaviour on Windows. Q-V5 rules out a Windows verify host.
  • It does not catch version skew, where an old actuator cannot decode new plan types. That was the other half of the Raven incident. Only the runbook's "rebuild at the installed sha" step covers it. A version handshake at hub attach would be the structural fix; it is noted and not mapped here.

Size. One Dockerfile line and one map line. It adds no targets.

R1 — NEVER TO BE WRITTEN: superseded by A1 (2026-09-30, Q-O1). Every routed target is proved by Idunn's own challenge whatever it declares, so the class guard below has nothing to guard. H1 (Heimdall responder) is still needed. The text is kept as history.

R1 (superseded) — A routed target proves it answers the route challenge before its fence (Idunn; Q-R1 ruled (a)).

  • Owner: Warming's last step, the transition to Fencing (:5742).
  • Inputs:
    • the candidate endpoint;
    • ReadinessClass::of(expected);
    • warming: RouteProofDirect and FirstOdinDirect are already a direct answer.
  • Rule. If expected.route.is_some() and warming is OdinTopology, then challenge_candidate(&current.value, &["warming", "active"], None) (:7313) must answer before transition(Fencing):
    • Silent becomes record_gate_wait, since the candidate may still be binding;
    • Refused becomes Err, which resume turns into a pre-fence abort (:4587-4593) with "routed target does not answer the stable-route challenge; a routed recipe must serve CultNet snapshot challenges";
    • an Answered result is not persisted. It is not readiness, and passing Warming is the record.
  • Deletes: none. This is a check moved earlier, not a second authority. The post-fence proofs at Routing and Commit stay the route's admission.
  • Size: about +20 production lines, no schema change.
  • Tests (Engine, B3 RoutedWorld with provides_odin = false and an Odin-correlated recipe):
    1. The stub's reply answers 404: the transaction pre-fence aborts from Warming, no lease is granted, and nothing is fenced.
    2. hang_up on: a gate wait, not an abort. Then hang_up off: it reaches Fencing.
    3. An honest stub reaches Fencing exactly as today.
    • Mutations it must kill: the class guard widened to RouteProof only (test 1 then reaches Fencing); Silent treated as Refused (test 2 aborts).
  • Live: nothing changes for ghostlight or streampixels-service, which answer today. Heimdall (not admitted) now fails in Warming instead of wedging post-fence.

B3 — Route-proof readiness (Idunn). For a route-proof target:

  • Warming is a direct candidate-endpoint challenge. This generalizes the FirstOdinDirect path and request_candidate_runtime_presence beyond Odin.
  • Ready is a direct candidate challenge returning active.
  • Routing and commit use the stable challenge.
  • Commit writes RouteProof readiness.
  • No Odin read occurs anywhere on the path, including the graph gate.
  • Odin-correlated targets are unchanged.
  • Negative check: with Odin stopped, a route-proof target admits, restarts, and keeps its route. An Odin correlation about it has no effect on any of its decisions.

B4 — Provider currency from Idunn's own observation; 9f00e7a reverted (Idunn). Now B4′ (section 4, Odin-authority cuts), unblocked by the audit and Q-O1: its unrouted Odin-class branch and the Odin-veto clause are gone.

  • Per Q2(b), validate_selected_providers_current and plan compile read the provider's current route observation within max age, and read capabilities from its signed presence.
  • Unrouted providers use their Q1 class evidence, authenticated now.
  • Revert the admission-time authentication.
  • ManagedReady carries a class-tagged evidence digest.

B4 status, 2026-09-30: was blocked on the Odin-lifecycle authority audit; now B4′ after A1 (a separate Imagination pass), not on Q-B4. Q-B4 is ruled in part (section 3): routed providers are current by Idunn's route proof and not degraded; unrouted providers by admission plus a live workload observation. The clause "a current Odin non-Ready word still excludes a provider" is held open. B4 also takes B2's degraded state as input.

C2 run 2 failure mechanism (Eyes, 2026-09-30)

The code was read at 583b2a7. The Idunn journal on Yggdrasil was read read-only; no state store was read.

Journal evidence.

  • 21:15:18: "excluded non-current provider streampixels-service: admitted provider's latest receipt is not Ready at its admission time".
  • 20:44 and 21:15: "…streampixels-web: admitted generation of streampixels-web is not Odin-correlated".
  • ghostlight and raven-muninn are excluded with the "latest … not Ready" text on every compile since 19:45, with Odin up.

The path.

  1. Plan compile (advance_sealing, :5375-5385) builds its provider list only from current_ready_provider_tokens (:7398-7415), which calls rehydrate_admitted_ready (:7417-7470).
  2. rehydrate_admitted_ready:
    • requires Odin receipts (odin_receipts(), :7422). A route-proof provider fails here with :2019, so no route-proof target can be a provider today;
    • requires the admitted latest_odin_observation to be semantically Ready (:7453-7456).
  3. refresh_admitted_topology writes every fresh authenticated correlation as latest, Ready or not (:5320-5325). Only a Ready one also moves ready.
    • So one non-Ready word from Odin removes the provider until Odin says Ready again.
    • Such a word can come from the provider's presence ageing out in Odin, including through Odin's own faults, for example its 64-slot RUDP session table.
    • With Odin stopped, that last word is frozen.
  4. managed_ready_provider_refs (deployment_plan.rs:264-316) takes capabilities from Odin's record. select_dependencies then bails "no expected provider satisfies streampixels.service.api http.v1 v1" (deployment_plan.rs:447-455).
  5. The same rehydration gates currency again at Starting, Routing and Committing, through validate_selected_providers_current (:7472-7534). So a deploy that compiled can still fail later on Odin's word.

This is the unlanded B4. Q2 was ruled (b): "Idunn's own current route observation of the admitted provider … Unrouted providers fall back to their Q1 class evidence".

  • streampixels-service is routed, so under B4 its currency is Idunn's route proof. C2 run 2 would admit.
  • "Both runs must admit" is therefore blocked on B4, not wrong.

One B4 brief item found here: challenge cadence against max age.

  • S1 challenges a healthy route only once its observation is older than topology_maximum_age_millis (30 s) (:5213-5221).
  • So a healthy route's observation ranges from 0 to about 30.5 s old.
  • A B4 check of "within max age", repeated at compile, Starting, Routing and twice at Commit, would sometimes refuse a healthy provider.
  • B4 must define currency as "the route is not degraded, and the last proof is within 2 × max age", or challenge on demand. That is a brief decision, not an operator one.

B5 — Every post-fencing phase ends (Idunn). 2026-09-30: B5 loses its Odin arm (the Odin part of the F16 fix) once A3 lands; route-proof targets never entered that path.

  • A deadline resolver runs before advance_transaction.
  • For stateless targets, expiry aborts to the incumbent through the existing post-fence path.
  • For stateful targets, expiry resolves per Q3.
  • A granted lease with no adoption evidence at its deadline becomes the named failure lease-not-adopted.
  • Delete resumable-forever: record_resumable_error after Fencing only annotates until the deadline.
  • Fix the F16 livelock. Commit from the topology admitted after the final proof when it is still Ready, instead of returning. Route-proof targets never enter that path.
  • Add the Q4 expiry request.
  • Negative checks:
    • An AwaitingReady state with no evidence resolves at its deadline.
    • An unadopted lease resolves without an operator.
    • Commit completes while Odin re-stamps every tick.

Q3-ii cut: snapshot before Ready, roll forward after

Imagination, 2026-09-30. Nothing has landed. It maps the Q3-ii ruling, verbatim: "D before Ready, E after; this actually closes the hole, and is a nice feature for Idunn to have". Q3-i is also ruled: any issued lease counts as possibly written.

Anchors:

  • Idunn hands/b5 = c06ff2b (src/ unless named; main's src/ = a96ad9d, so B5 is the only code delta);
  • gamecult-ops 0961976, StreamPixels a49f7d8, Heimdall e8e2832, Ghostlight and Odin working trees (recipes read only).

"Probe" means one of:

  • a read-only command on Yggdrasil, 2026-09-30 about 16:00 UTC (18:00 CEST); nothing was written to the host;
  • a throwaway postgres:16 container on Starfire, destroyed after the run.

"Read" means source at those heads.

Q3-ii.1 Body facts

  • S1. Fence order (read). The candidate starts in Starting and runs alongside the incumbent through Warming (candidate-then-promote).

    • Fencing (advance_fencing :6712-6797) stops the incumbent and revokes its lease. It does this only when the incumbent holds a lease (:6719, :6733-6750). The phase then moves to Leasing (:6796).
    • Leasing's first write is LeasingEvidence::Prepared (:6953-6963); under Q3-i, Prepared already counts as issued.
    • Ready is written in AwaitingReady together with lease_adoption: route-proof at :7018, Odin-correlated at :7069. Routing only refreshes an existing Odin receipt (:7118). So ready.is_some() is monotone inside a transaction, and it is the Ready boundary.
  • S2. An unleased incumbent is not fenced (read). A stateful candidate replacing an incumbent without a lease (for example the first deploy after a recipe declares state) leaves the incumbent running until post-commit cleanup. incumbent_was_stopped_during_fencing (:9058-9066) derives "stopped" from the incumbent's lease digest.

  • S3. Candidate writers before the lease (read).

    • StreamPixels loads the application, and with it runPostgresMigrations, only after waitForWriteLease (deployment/idunn/runtime-presence.mjs:58-59).
    • Heimdall builds its app, which runs createStore, and then ensureSchema, before any lease check (src/index.ts:18, src/store/index.ts:16-18). It also writes runtime-presence-sequence into its state root on every health pulse, lease or not (src/index.ts:115).
    • Recipes declare CultCache-file slots and validate that writable slots open after the lease (deployment.rs:960-969). Postgres is declared nowhere. StateRecovery and MigrationDeclaration are parsed and never consumed by the control plane (grep).
  • S4. Out-of-band writers (probe).

    • pg_stat_activity on streampixels holds one session: pid 799973, which is unit idunn-streampixels-service-8c469c…, the admitted Idunn workload.
    • The heimdall database is held by the legacy heimdall.service, which Idunn does not manage (secret-files map P8).
    • No timer or cron touches either database.
    • Operator scripts apps/service/scripts/create-creator-invite.ts and grant-operator-role.ts write streampixels when an operator runs them.
  • S5. Sizes and times (probe).

    Store Size Snapshot
    streampixels database 9.0 MB pg_dump -Fc 137 ms, 48 KB archive
    heimdall database 38 MB pg_dump -Fc 1,042 ms, 13.7 MB archive
    streampixels-service state root 12 KB
    odin state root 21 MB
    ghostlight-world-v2 state root 204 KB
    heimdall state root 12 KB

    Free disk is 1.6 TB. Postgres is 16.15, on local peer auth.

  • S6. State roots do not match their recipes (probe).

    • ghostlight-world-v2/service/play-turn-v1.cc (plus its .lock) is live and undeclared in the Ghostlight recipe.
    • Heimdall's state root holds provider-health-identity.cc (a private identity, mode 0600) and runtime-presence-sequence. It does not hold its declared heimdall.service.cc; Heimdall is not admitted.
    • The installed odin binding's state_root is /var/lib/gamecult/odin. The template idunn/yggdrasil/bindings/odin.toml.in says odin-v2, which does not exist on the host. That is template drift (ops follow-up).
  • S7. Postgres restore mechanics (probe, container).

    • In-place pg_restore --clean --if-exists --single-transaction leaves tables the candidate created: the probe's sessions table survived. An in-place restore is not exact.
    • A side database restored with --no-owner --role=<owner> and swapped with two ALTER DATABASE … RENAME in one transaction block swaps atomically and leaves no candidate residue.
    • RENAME refuses while another session is connected: it waits 5 s, then fails "being accessed by other users". So the swap terminates the database's sessions first.
    • Fingerprint. Take the sha256 of the archive's text (pg_restore -f -), with -- comments, blank lines and the per-dump random \restrict/\unrestrict lines removed. It equals the same fingerprint of pg_dump of the restored database and of the source. Changing one row changes it.
    • Neither live database has database-level ACLs, role settings or extensions (probe). pg_hba is local all all peer, so root reaches Postgres through runuser -u postgres with no secret, as the backup producer already does.
  • S8. Supervision is blocked for a live transaction (read).

    • While any transaction of the target is at or past Fencing and not yet Complete, the target's supervision pass skips it entirely (:5684-5701). That includes a running post-fencing abort, whose phase stays below Complete.
    • After the terminal write, supervision repairs the Expected projection (:5745) and mints continuity (:5987).
    • So a restore inside the abort cannot race continuity. A restore that fails and then goes terminal can: the next mint restarts the incumbent on half-restored state.
  • S9. The abort path has no deadline (read).

    • resolves_by_deadline excludes post_fencing_abort (:1568-1573), and each abort step retries every tick.
    • route_restoration retries ufw delete forever. B5's own test :18394 pins 20 attempts and no end.
    • The abort order is route, lease, candidate, topology, source (:8717-8858). A state restore placed after the route step would inherit that livelock.
  • S10. Commit and route supervision need a promoted route (read).

    • AdmittedGeneration::from_transaction requires Committing, Ready and a routing disposition (:2370-2418).
    • Route supervision bails without RoutingEvidence::Promoted (:6049-6055).
    • Its repair restore_admitted_membership (:6130) installs from a membership digest and reloads only when the fragment differs (6ff7d5a).
  • S11. Continuity re-admits stored plans under current rules (read). parsed_inputs runs binding.admit (deployment_plan.rs:634-643). A new admission rule must therefore be compile-time only, as secret-files §2 already records.

  • S12. Sandbox (read, and the installed unit probed identical). Idunn runs as root with ProtectSystem=full. /var/lib/gamecult/idunn and every target's state root are ReadWritePaths, so a snapshot directory under Idunn's root needs no unit change.

Q3-ii.2 The exactness assumption (question 1)

The assumption is that the incumbent cannot write between the fence and the lease. It holds for Idunn-managed writers under three conditions. The body breaks two of them today.

  1. Every live incumbent is stopped at the fence when the candidate is stateful. Broken by S2. Fix: when expected.write_lease_required, Fencing stops any exact incumbent, leased or not. FencingEvidence::Revoked records incumbent_stopped: bool, and incumbent_was_stopped_during_fencing reads that field instead of deriving it from the lease.
  2. The candidate writes nothing before its lease.
    • This is declared and validated for CultCache slots.
    • It is not enforced for Postgres. StreamPixels complies. Heimdall does not: its schema DDL runs at start, and its sequence file is written every pulse (S3).
    • The fence cannot cover this. It is a target-contract fault, and the fixes belong in Heimdall (follow-ups H-S1, H-S2 below).
  3. No out-of-band writer. Today that means the legacy heimdall.service and hand-run StreamPixels operator scripts (S4).

What breaks when it is not exact:

  • A restore silently discards whatever an out-of-band writer committed after the snapshot.
  • If the candidate migrated before the fence, the "incumbent's state" captured is already the candidate's.

What closes it. Condition 1 is closed in code. For conditions 2 and 3, the snapshot drivers check the no-writer precondition at the store. They check at snapshot start, again after the capture, and again immediately before Leasing writes Prepared:

  • a Postgres store requires zero sessions on its database;
  • a file store takes every captured file's .lock exclusively and non-blocking; a held lock means a live owner.

A failed check is a typed refusal inside Snapshotting. No lease is issued, so the deadline ends it as a plain abort, with nothing to restore.

With the current body, this refuses every Heimdall deploy until H-S1 lands. That is the correct outcome.

One residual stays open: a writer that connects and disconnects between two checks goes unseen. Only Q-S3 (b) closes that.

Order in the phase machine after this cut: Fencing (stop every incumbent, revoke) → Snapshotting (new) → Leasing (Prepared = issued) → AwaitingReady (Ready durable = the D/E boundary) → Routing → Committing.

Q3-ii.3 Model page additions

Kind What names it Life over time Who decides
Snapshot declaration ([state_snapshot] in the operator binding) the binding's target; frozen into the plan through binding_blob Changes only by an ops binding change and a new Deploy plan. An old plan without it keeps running continuity (S11) gamecult-ops template; Idunn validates shape at parse and coverage at compile
Snapshot evidence (transaction key 37, state_snapshot) the transaction; one per Deploy past Fencing Written once, when every store is captured and digested. Read by the restore. Terminal with its transaction Engine (Snapshotting). Forbidden: CLI, drivers writing the record
Snapshot bytes (/var/lib/gamecult/idunn/snapshots/<target>/<transaction_id>/, 0700 root) path derived from the transaction id Rewritten whole on a Snapshotting replay. Deleted by the transaction's own terminal cleanup step, except under a state hold, where they are deleted when the hold is released. No janitor the transaction that owns the path
State restoration (PostFencingAbort.state_restoration) the abort Skipped (no lease issued) → Pending{deadline_at} → Complete{proof} or Failed{store, kind} abort path. Forbidden: supervision, CLI
State hold (TargetSupervision.state_hold, supervision v2) target Set in the same CAS as a terminal OperatorRequired{RestoreFailed}. Released per Q-S4. Never set by anything else Engine at the terminal write; release per Q-S4. Forbidden: continuity mint, which only reads it
Roll-forward record (transaction key 38 roll_forward intent; completion Admitted{recovery: RollForward}) the transaction Set by the resolver after Ready. Terminal at the forced commit deadline resolver

Q3-ii.4 Snapshot/restore contract (question 2)

Binding shape. It is typed and closed; there are no command strings. Idunn accepts no imperative input: see the unit's own comment and cli_exposes_only_declarative_commands.

[state_snapshot]
snapshot_seconds = 120   # optional; Idunn default 120. The Snapshotting deadline
restore_seconds = 300    # optional; Idunn default 300. The abort's restore deadline

[[state_snapshot.stores]]
kind = "state-slots"     # the recipe's writable slots under workload.state_root (scope: Q-S2)

[[state_snapshot.stores]]
kind = "postgres"
database = "streampixels"   # local socket, peer auth as OS user postgres; no URL, no credential file
  • Parse (deployment.rs, OperatorBinding :266-287, new StateSnapshotBinding):
    • duration bounds as in DeadlineBinding (:734-750);
    • database names are identifiers;
    • no duplicate stores;
    • state-slots requires a workload.state_root.
  • Compile-time coverage goes in compile_deployment_plan (deployment_plan.rs:674-722), never in admit() (S11):
    • a stateful recipe with a state-slots store must declare writable file slots;
    • the no-declaration case is Q-S1.
  • The durations live in the binding table and are frozen by binding_blob. PhaseDeadlines is untouched, so no v3 plan changes its plan_id.

Where the bytes live, and who owns and prunes them. snapshots/<target>/<transaction_id>/ under Idunn's own root:

  • slot copies keep their relative path, owner and mode;
  • postgres/<db>.dump is 0600.

The manifest is not a file. It is the typed snapshot evidence on the transaction:

  • per slot: relative path, size, mode, uid, gid, sha256, and whether it existed;
  • per database: archive sha256, fingerprint (S7), owner role, and the zero-session check times.

Pruning:

  • the transaction deletes its own directory in a new last cleanup step, on both PostCommitCleanup and PostFencingAbort;
  • under a state hold it is kept, and deleted on release.

The path is derived from the transaction id, so a crash leaves nothing a later step cannot find. gamecult-ops adds /var/lib/gamecult/idunn/snapshots to the backup EXCLUDES. It duplicates live stores already captured, and it can hold state-root private identities (S6).

Budget. The write pause (fence stop → lease) grows by the capture time. Measured today: about 0.2 s for streampixels-service and about 1 s for heimdall (S5). The defaults leave two orders of magnitude of headroom.

Secrets.

  • Idunn never opens a service credential file. Postgres is reached by peer auth, so the database URL files stay unread; this keeps secret-files §1 "Idunn never opens a service credential for reading".
  • pg_dump/pg_restore stderr can quote row data, and idunn status prints last_error verbatim (secret-files P7). So no tool stderr enters last_error, report_once or the journal. The error carries a closed SnapshotFault kind, the exit status, and the stderr's sha256. The full stderr goes to snapshots/…/stderr-<step> (0600), which is pruned with the directory.

Idempotence and crash safety.

  • Snapshotting is repeatable while the fence holds. A crash before the evidence write means the next tick deletes the directory and captures again.
  • Restore is decided from observable state, never from a sub-step log.
    • Postgres: if the live fingerprint already equals the recorded one, the store is done. This covers a crash after the swap. Else, if <db>__idunn_restore exists and its fingerprint equals the recorded one, terminate the database's sessions, swap both names in one transaction, and drop the displaced database. Else drop and recreate the side database from the archive.
    • Slots: write each one by temp file and rename in its own directory; remove a slot file that did not exist at snapshot time; leave .lock files alone.
    • Replaying any prefix of these steps converges.

How restore proves itself.

  • Postgres: fingerprint equality on the side database before the swap, so a bad restore never swaps. After the swap, the live name resolves to the side database's oid.
  • Slots: a re-walk equals the manifest byte for byte, including absences.
  • The proof digests go into state_restoration: Complete{proof}.

No declared snapshot: Q-S1.

Q3-ii.5 Phase machine (question 3)

  • Snapshotting is a new DeploymentPhase variant between Fencing and Leasing (:206-216, Ord-derived).

    • PhaseDeadline::entering (:913-934) gains its arm from snapshot_seconds.
    • resolves_by_deadline (:1568-1573) covers it through the existing range.
    • It is entered only by a Deploy whose plan declares a snapshot. Continuity and stateless targets go Fencing → Leasing as today.
    • Expiry in Snapshotting → Abort{restore: false}. The partial directory is removed by the abort's cleanup.
  • Restore is a step, not a phase. PostFencingAbort (:1382-1400) gains state_restoration and deadline_at_unix_millis.

    • begin_post_fencing_abort (:8694-8712) stamps the deadline. For a restoring abort that is restore_seconds; for any other abort it is an Idunn default of 300 s (constant).
    • Order becomes lease → candidate → state → route → topology → source. Restore needs only the candidate stopped and its lease revoked, and S9's livelock must not hold state hostage. "Never two writers" is about lease-then-process, and that order is preserved.
  • Every abort ends.

    • On the abort deadline, if state_restoration or any step before it is still Pending, the transaction completes FailedAfterFencing{OperatorRequired{RestoreFailed}}. A restore that cannot even stop the candidate is a failed restore.
    • Steps after it (route, topology, source) are marked a new CleanupEvidence::Abandoned, reported once. The transaction completes with its normal recovery.
    • Abandoning those steps is safe because supervision owns their repair: Expected repair and demotion (:5745, :5943-5985), and the admitted-route restore (:6130).
    • This ends B5's ufw delete loop for every abort, not only D's.
  • The Ready boundary is transaction.ready.is_some() (S1). PhaseEnd is rebuilt as follows. The B5 table test is rewritten to it.

    Command Ready durable Lease issued (Prepared or Granted) End
    Deploy yes any RollForward
    Deploy no yes Abort{restore: true} (snapshot evidence required by validation)
    Deploy no no Abort{restore: false}
    Continuity any any Abort{restore: false}, as B5

    Continuity keeps B5's arm. Its candidate is the admitted release, it restarts under the restart log, and it never changes the state contract. That is a default, not a question.

  • E replaces the abort after Ready. It is a forced commit, and it only writes the store.

    • The resolver persists roll_forward = Some{cause, at} and, if needed, enter_phase(Committing). resolves_by_deadline excludes a record with roll_forward, so it fires once.
    • advance_committing (:7206-7392) with roll_forward set skips:
      • the final route proof;
      • Odin re-admission;
      • the workload observation;
      • lease currency.
    • It then commits the generation from the durable evidence:
      • Routing with no route receipt: a routed target gets a new RoutingEvidence::Unproven{membership_sha256}, the digest of the rendered fragment. An unrouted target gets SkippedUnrouted.
      • Committing with Promoted: kept.
    • Unproven starts route_supervision degraded. Supervision (:6037-6140) accepts Unproven: it repairs with restore_admitted_membership, proves, and on success rewrites the receipt to Promoted.
    • A candidate that died after Ready is admitted anyway, and continuity restarts it under the restart log. That is "the release supervision keeps alive", literally.
    • Boundedness. The forced commit does no actuation. Every later actuation is supervision's: route repair spaced by the challenge backoff up to ROUTE_CHALLENGE_BACKOFF_CAP_MILLIS (:92) and reloading only on a fragment difference; process restarts bounded by the restart log (:69-72). No post-fence path retries forever.
  • D and TargetSupervision.

    • The restore runs inside the live transaction, so supervision cannot mint over it (S8).
    • A RestoreFailed terminal sets state_hold in the same CAS as the completion (a three-envelope CAS like the mint at :5987-6031).
    • The mint path checks the hold right after restarts_exhausted (:5916) and mints nothing, with one report.
    • Expected repair may still run; it starts no process. Deploy commands stay admissible (Q-S4).
  • Terminal records.

    • FailedAfterFencing{recovery}, where recovery is one of:
      • RestoreIncumbent: D restored, or no lease was issued;
      • RestartAdmitted: continuity, as B5;
      • OperatorRequired{reason: OperatorRequiredReason::RestoreFailed{store, kind}}: typed. B5's free string reason has no production writer (watchdog map I5).
    • Admitted{generation_id, recovery: Option<TerminalRecovery>}, with Some(RollForward{cause}) only there.
    • Validation (:2040-2075, :2180-2205) pins each variant to its completion.
  • Schemas and lifts. One store-version install.

    • deployment_transaction.v5 carries:
      • the Snapshotting variant;
      • keys 37 state_snapshot and 38 roll_forward;
      • FencingEvidence::Revoked.incumbent_stopped;
      • PostFencingAbort.{state_restoration, deadline_at_unix_millis};
      • CleanupEvidence::Abandoned;
      • the completion shapes above.
    • The v4 lift (the read_transaction_record pattern, :3659-3704):
      • incumbent_stopped = incumbent_lease_sha256.is_some();
      • state_restoration = Skipped;
      • an in-flight abort's deadline is stamped once on first sight (B5's rule for lifted phase deadlines, :8588-8596);
      • Admitted.recovery = None.
    • History is lifted unchanged.
    • admitted_generation.v5 carries RoutingEvidence::Unproven. target_supervision.v2 carries state_hold, lifted as None.
    • Every lifted fixture must re-encode canonically: the B2 F1 scar.
    • An older Idunn refuses the v5 store, so rollback is the pre-install backup, as for B2.

Q3-ii.6 What B5 keeps, changes and deletes (question 4)

  • Keeps:
    • the resolver's shape (resolve_phase_deadline :8568-8623);
    • PhaseDeadline and enter_phase as the only deadline writer;
    • idunn expire and idunn.expiry_request.v1. Before Ready it now restores; after Ready it rolls forward, through the same resolver;
    • lease-adoption evidence, lease-not-adopted, and the continuity arms;
    • the F16 commit fix and the reload skip.
  • Changes:
    • phase_end (:1583-1596), terminal_recovery (:1598-1606) and resolves_by_deadline (:1568-1573), to the table above;
    • the resolver's match (:8600-8620);
    • incumbent_was_stopped_during_fencing (:9058-9066);
    • post_fencing_abort_intent (:9081-9110);
    • the abort order (:8717-8858).
  • Deletes (the "left running" arm):
    • PhaseEnd::OperatorRequired and its doc (:943-945);
    • the CommandKind::Deploy if issued arm (:1588);
    • the resolver's OperatorRequired branch (:8610-8619, the deadline:<tx> report);
    • the "left to the operator" comment (deployment_plan.rs:470-473);
    • the three OperatorRequired rows of an_ended_phase_aborts_unless_a_deployment_candidate_was_issued_a_lease (:12887-12889); the test is rewritten to the Ready table;
    • the string-reason test an_operator_required_recovery_must_say_why (:12911-12935), rewritten for the typed reason.
  • Recommendation: merge B5 first, after its own Soul pass. This map is a follow-on cut (Q-S5).
    • B5 bounds the live continuity reload loop (B2-3d F1).
    • Its left-running arm is no worse than today's resumable-forever post-fence.
    • D and E carry two schema bumps and a new driver, and need their own falsification.
    • Cost of the gap: a stateful deploy that expires after a lease stays live and reported, as today. idunn expire re-enters that arm (watchdog I6). The StreamPixels-service redeploy in the ship sequence runs under that behaviour.

Q3-ii.7 Cuts

Order: SR-1 → SR-2 → SR-3 → SR-4. The build and tests run on Yggdrasil (ygg-verify.sh) against the single idunn crate. The build activates no new target.

  • SR-1. Snapshot drivers (Idunn drivers.rs, deployment.rs). No phase-machine change.
    • One port: StateSnapshotPort { capture, check_no_writer, restore, prove }.
    • Two implementations:
      • SlotSnapshotDriver: files, with .lock exclusivity;
      • PostgresSnapshotDriver: runuser -u postgres for pg_dump -Fc, pg_restore, psql; side database, fingerprint, swap. The program paths are injectable, as the route stubs are.
    • The binding table parse and validation.
    • The closed SnapshotFault kind and the stderr quarantine.
  • SR-2. D (Idunn control_plane.rs, deployment_plan.rs).
    • Deletes first: B5's arm (Q3-ii.6).
    • Then:
      • the Fencing change (Q3-ii.2 item 1);
      • the Snapshotting phase;
      • the restore step, abort deadline, reorder and Abandoned;
      • the state hold;
      • the compile-time coverage rule;
      • all three schema bumps and lifts, including E's shapes, so the store moves once.
  • SR-3. E (Idunn control_plane.rs).
    • the RollForward end and the forced commit;
    • Unproven in Routing and supervision;
    • Admitted.recovery.
  • SR-4. Ops (gamecult-ops).
    • [state_snapshot] for streampixels-service (state-slots + postgres streampixels), ghostlight (state-slots), odin (state-slots), and the Heimdall template (state-slots + postgres heimdall);
    • the backup exclude;
    • fixing the odin template's state_root drift (S6).
  • Owning-repo follow-ups (not in this campaign's cuts):
    • H-S1 Heimdall opens Postgres and runs ensureSchema only after its lease.
    • H-S2 Heimdall moves runtime-presence-sequence out of its state root; it is runtime identity, not state.
    • G-S1 Ghostlight declares service/play-turn-v1.cc as a slot (under Q-S2 (a)).
    • The legacy heimdall.service retires before Heimdall's first Idunn deploy (secret-files Cut 5).

Q3-ii.8 Authority map

  • Owner.
    • The deadline resolver owns how an ended phase resolves (restore, roll forward, or plain abort).
    • The abort path owns the restore.
    • The Engine's Snapshotting step owns the snapshot.
  • Inputs:
    • the plan's frozen binding ([state_snapshot]);
    • the recipe's slots;
    • ready, leasing and fencing evidence;
    • the phase and abort deadlines;
    • expiry requests;
    • the stores themselves, through the port.
  • Outputs:
    • snapshot evidence;
    • state_restoration;
    • the terminal completion with its recovery;
    • state_hold;
    • an admitted generation with Unproven routing.
  • Derived state:
    • Q3-i's "possibly written" is derived from leasing;
    • "stopped at fence" is derived from incumbent_stopped and no longer from the lease digest;
    • the snapshot directory is cache-only, and its truth is the evidence digests;
    • report_once text is notification-only.
  • Forbidden writers:
    • supervision writing restore or snapshot state, or minting over a held target;
    • the CLI writing any of it (expire writes only its request);
    • drivers writing records;
    • the backup producer restoring anything;
    • a route or topology step deciding the recovery.
  • Shared paths: deadline expiry and idunn expire go through one resolver. Direct and Odin-correlated Ready use one boundary field. D aborts and plain aborts use one abort path with one deadline rule. The forced commit and the normal commit use one from_transaction.
  • Deletion line:
    • B5's OperatorRequired left-running arm;
    • lease-derived fence-stop inference;
    • the unbounded abort step.

Q3-ii.9 Verification

Each test pins a rule. Each must be killed by the named mutation.

Test (behaviour) Rule pinned Mutation that must kill it
a deploy that expires before Ready restores the snapshot, and the incumbent restarts on the pre-lease bytes D restore marks Complete without writing; or PhaseEnd returns Abort{restore: false}
continuity mints nothing while the restore is Pending S8 widen the supervision skip range's lower bound past the abort
a failed restore is terminal OperatorRequired{RestoreFailed}, and supervision mints no restart only terminal operator case; hold drop the hold check, or set the hold outside the completion CAS
a deploy that expires in Routing after Ready is admitted Unproven, restores nothing, and supervision later promotes it E Ready-side arm returns Abort
an expiry in Committing rolls forward from the promoted receipt E forced commit rewrites the routing receipt
the Ready table: (Deploy, Continuity) × ready × (no lease, Prepared, Granted) boundary and Q3-i treat Prepared as unissued
idunn expire before Ready restores; after Ready it rolls forward shared path a separate expire branch
the fence stops an unleased incumbent when the candidate is stateful exactness 1 revert the :6719 condition
a held slot lock, or a live Postgres session (fake runner), refuses Snapshotting with no lease issued exactness 2/3 skip either check, or check only at start
crash replay at every snapshot and restore step converges (persist the prefix, rerun) idempotence the restore trusts a sub-step flag over observation
the restore removes a slot file the candidate created exact restore copy-over only
an abort whose ufw delete fails forever ends at its deadline: state restored first, route Abandoned, recovery RestoreIncumbent every abort ends route before state; or Abandoned allowed on the state step
a v4 store (every fixture) lifts to v5, and supervision v1 to v2; each re-encodes canonically schema drop a field default
a stateful Deploy with no [state_snapshot] is refused at compile, and continuity of an old admitted plan still runs Q-S1 (a); S11 put the rule in admit()
a canary in fake pg_restore stderr appears in no last_error, report or status line secrets copy stderr into the error
Postgres integration, #[ignore], run on Yggdrasil against a scratch cluster or container: side-database restore drops candidate tables; fingerprint mismatch refuses the swap; the swap terminates a live session; a crash between swap and drop replays S7 in-place --clean restore

Then run cargo mutants --in-diff.

Only a live Yggdrasil rehearsal, with the operator present, can show:

  • runuser/pg_dump reach the socket under the real unit sandbox;
  • the snapshot directory is written under the real ReadWritePaths;
  • a streampixels-service Deploy from a scratch branch whose candidate migrates and never reports Ready restores streampixels, after which the incumbent serves again. Use a shortened awaiting_ready_seconds in a scratch binding;
  • the measured write pause;
  • E: a Ready candidate whose stable-route proof is blocked is admitted Unproven, and is promoted by supervision once unblocked;
  • the backup run skips snapshots/.

Q3-ii.10 Subtraction ledger (estimate)

Production Tests
Deleted about -40 (B5 arm, comment, lease-derived stop inference) about -30 (rows, string-reason test)
SR-1 drivers and binding about +450 about +300 (fake runners), +120 (ignored Postgres integration)
SR-2 D, lifts, hold, abort deadline about +450 about +450
SR-3 E about +180 about +250
SR-4 ops about +20 of binding TOML, 1 backup exclude line none
  • Net: about +1,050 production and +1,100 test lines in Idunn. It adds two schema bumps plus one supervision bump, installed as one store move, and no new target, service or dependency (it uses the host's pg_dump, pg_restore and runuser).
  • What it buys: the ruled capability, which closes Q3-ii's hole, and an end for every post-fence path, including B5's abort livelock.
  • What it retires:
    • the operator-only exit for stateful deploys;
    • the b5-operator-required-no-exit follow-up (watchdog map).

Q3-ii.11 Watchdog interface (question 5)

The interface is a single point. The operator-required incident opens at the terminal CAS that writes FailedAfterFencing{OperatorRequired{RestoreFailed}} and sets state_hold; its subject is the transaction id, as the watchdog map models it. It closes when that hold is released (Q-S4). Nothing else in this cut opens or closes it.

Q3-ii.12 Operator questions

Rulings, operator, 2026-09-30: "S1: Refuse / S2: Declared state / S3: Detect and refuse / S4: deploy or release". That is Q-S1 (a), Q-S2 (a), Q-S3 (a), Q-S4 (a). Q-S5: "B5 first", which is (a): B5 merges after its Soul pass, and SR-1..SR-4 follow as their own cuts.

  • Q-S1. A stateful target whose binding declares no snapshot.
    • Options:
      • (a) Refuse a new stateful Deploy plan at compile, with a typed error. Continuity of already-admitted plans is unaffected.
      • (b) Roll forward even before Ready: no D for that target.
      • (c) Keep B5's left-running report.
    • Recommend (a). D is only a guarantee if it cannot be skipped by omission. (b) quietly reopens the hole this ruling closed.
    • Depends on it: SR-2's compile rule, SR-4's binding list, and whether Heimdall can deploy before H-S1.
  • Q-S2. What state-slots captures.
    • Options:
      • (a) The recipe's declared writable slots only. The recipe is the authority on what is state. Undeclared files in the state root are reported once and not restored.
      • (b) The whole state-root tree.
    • Recommend (a). It matches "the recipe declares; Idunn infers nothing".
    • Under (b), Heimdall's runtime-presence-sequence would be restored backwards, and its presence sequence would regress (S6).
    • Cost of (a): Ghostlight must declare play-turn-v1.cc (G-S1). Until then, a failed Ghostlight deploy restores everything but that file.
  • Q-S3. How hard the fence holds against out-of-band writers.
    • Options:
      • (a) Detect and refuse: zero sessions and free locks, checked at snapshot start, after the capture, and immediately before Prepared.
      • (b) Also block: ALTER DATABASE … ALLOW_CONNECTIONS false from snapshot to grant. Idunn owns the undo on grant, abort and crash replay.
    • Recommend (a). Today's only out-of-band writers are the legacy Heimdall unit, which retires, and hand-run scripts. (b) adds a database-configuration mutation that Idunn must always undo, and a missed undo locks the service out of its own database.
    • Depends on it: SR-1's scope and the residual in Q3-ii.2.
  • Q-S4. What releases a state hold after a failed restore.
    • Options:
      • (a) A successful Deploy commit of that target, or idunn release <target>. The verb writes a typed request the daemon consumes, as idunn expire does, after the operator has repaired the state by hand from the retained snapshot.
      • (b) A Deploy commit only.
      • (c) The verb only.
    • Recommend (a). A deploy is already an operator act that captures its own snapshot. The verb covers "I fixed it by hand; bring the incumbent back" without forcing a redeploy.
    • Depends on it: SR-2's hold, the retained snapshot's pruning, and the watchdog incident's closing event.
  • Q-S5. Merge order.
    • Options:
      • (a) Merge B5 after its Soul pass; SR-1..SR-4 follow as their own cuts.
      • (b) Hold B5 until D and E land.
    • Recommend (a), for the reasons in Q3-ii.6. Under (a), the StreamPixels-service redeploy in the ship sequence runs with B5's report-and-wait on a stateful expiry.

Odin-authority cuts (Imagination audit, 2026-09-30)

Source: the Odin-lifecycle authority audit (anchors Idunn 94c7691/583b2a7, Odin 5c37860), folded in as section 5. It ends B4's "blocked on the audit" state. Sequenced against the B2 rework map. Line counts are estimates from function spans.

# Cut Repo Blocked by Subtraction / addition Live relevance
X1 Delete the dead lifecycle residue: O12 (idunn.rs plus the idunn.* schemas in documents.rs), O13 (JS coordinator, its tests, package.json start, start-/restart-odin.ps1), O14 (the other services' deploy, restart and health scripts), O15 (doc sections rewritten to the live body); gamecult-ops/compose/odin.yggdrasil.yaml; the idunn-deployment-targets.ps1 references to Odin scripts. IN PROGRESS on Odin hands/odin-x1-residue Odin, ops nothing about -7,000 lines (672 + 2,814 + 140 + 3,208 + docs), +~40 of docs none. Muninn keeps compiling at its pin; its next bump must drop IdunnDaemonHealthRecord (follow-up M2)
A1 Every routed target is proved by Idunn's own challenge, whatever it declares. ReadinessClass::declared: routed -> Direct. OdinSelf and OdinCorrelated apply only to unrouted targets until A2. Odin itself is routed, so its FirstOdinDirect path generalizes; delete the "is Odin" arms in Warming (:5614-5640). The recipes' Odin dependency flips to optional (Q-O4) Idunn (+ recipes) Q-O3; after B2 about -60 / +20 admitted ghostlight, streampixels-service and odin carry Odin receipts and become WrongVoucher-held on install (Q-O3). Continuity of ghostlight and service no longer needs Odin (fixes I2 for them). Replaces R1, which is never written (its class guard has nothing to guard). H1 (Heimdall responder) is still needed
B4′ B4 as mapped, with its rule collapsed: provider currency is Idunn's own proof, not degraded, last within 2 x max age. ManagedReady carries a direct-evidence digest. Capabilities come from the challenged presence. The held Q-B4 clause is struck Idunn A1 (or with it) B4's own estimate, minus the unrouted Odin branch C2 run 2 admits
A2 Unrouted targets get a challenge endpoint. The binding declares a private challenge endpoint without a stable route; request_runtime_presence_at (drivers.rs:5488) is reused against it; Expected carries it digest-bound. M1 (Muninn): raven-muninn answers SnapshotRequest for its presence on that endpoint (the StreamPixels C2 and Heimdall H1 shape) Idunn, Muninn, ops binding Q-O2 (ruled) about +80 Idunn, +~60 Muninn raven-muninn gains a proof path and leaves the held state after a redeploy
A3 Delete Odin consumption from Idunn entirely: every forbidden writer in section 5.3. Schema bump: transaction and generation lose latest_odin_observation, odin_publisher_sequence_cursor and odin_authority, plus the Odin enum variants. Boot stops requiring the Odin anchor. B5 loses its Odin arm: the Odin part of the F16 fix is deleted from B5's brief Idunn A1, A2, B4′; every admitted generation re-admitted Direct about -1,100 production and -1,500 to -2,000 test lines; +~100 of negative tests store migration, irreversible; back up control.cc and history.cc first. Fold the v2/v3 lift retirement in if the operator has retired those backups
O-R Odin stops originating readiness. Delete ready, dependency_evidence and dependency_permits_ready (O1-O3), and O7's re-verification. If nothing reads the correlation after A3, retire its publication, its signer and dedupe (O8), and OdinTopologyIdentity enrolment in idunn-provision Odin (+ Idunn provision) A3 about -150 (verdict only) to about -600 (the correlation retired) none after A3
L-R Retire the odin.runtime_topology_correlation.v2 contract in cultnet-rs (runtime_authority_contracts.rs:455-~1100: dependency evidence, record, authenticator), under CultLib's per-runtime rules. Only cultnet-rs implements it CultLib O-R about -400 none

Order.

  1. X1 lands any time (in progress).
  2. Then B2 as mapped, then A1 (Q-O1, after Q-O3 is ruled), and the redeploys for Q-O3. H1 is still needed; R1 is never written.
  3. Then B4′, then A2 with M1, then A3, then O-R, then L-R.
  4. Meanwhile S3 (the web drops Odin) proceeds unchanged. B5 loses its Odin arm. T1, T2, W1 and D1 are unaffected.

Net estimate. About -9,000 lines across Odin, Idunn and CultLib, of which about 7,000 is dead residue. Plus 1 Idunn schema bump (A3), about 200 lines added in A2 and M1, and about 100 lines of new negative tests.

Verification gates (Soul, per cut).

  • A1. With Odin stopped, a continuity of a routed target that declares Odin reaches Complete. An Odin correlation that says not-Ready about it changes nothing. A held Odin-voucher generation mints no continuity (existing F1 rule).
  • A3. rg -i odin src/ in Idunn returns only strings: projection doc text, and the capability name inside test recipes. Boot succeeds with no Odin anchor file. A store with a v4 Odin-voucher generation is refused or held, never re-proved.
  • O-R. No Odin code path computes a boolean named or used as Ready for another target.

Temporary rule, Odin (deletion line: A3 plus Q-O5's supervision restart). Until A3 and the Q-O5 restart land, Odin keeps three permanent self-publication failures fatal, as WriteLeaseLost is: no runtime authority, signer/anchor mismatch, and the fail-closed stored-presence branch (lib.rs:945-954). Without it eeb6812's survive would swallow them, Odin's self-correlation would freeze stale, and by O2/I8/I9/I2 every dependent would be excluded and stall in Warming, deploys and continuity alike; before eeb6812 the process exited and a restart cleared it. It is not a second lifecycle owner: it is Odin reporting its own death through the only signal Idunn acts on today, and CONTINUITY_RESTART_ATTEMPTS bounds the loop. Being added on Odin hands/odin-session-table.

Owning-repo cuts

C1 — Local route proof in the CultLib TS runtime (CultLib, after Q6).

  • Split idunn-runtime-authority.ts into:
    • authority loading and signing, with no Odin endpoint, no Odin dependency check, and no RUDP;
    • an optional Odin presence publisher for CultMesh-aware services.
  • publishRouteObservation becomes answerRouteObservation(request, state). It signs locally and returns the document immediately, with no network. The state comes from the app's current health (warming until first healthy) rather than a constant active.
  • The lease pickup contract is unchanged.
  • Tests are in the package. Verify under the node image on Yggdrasil.

C1 status, 2026-09-29 (Self). Landed on CultLib idunn-ts/route-c1 (795fd10). Soul closed it on conditions, and a fix batch is in Hands:

  • stateful, lease-bound Rust vectors;
  • the signer owns health (reportHealth), starting at warming;
  • the signer records every warming it signs, so a route-proof stateful target can take its lease;
  • one signer per authority, a frozen authority, and a subpath export without the Odin publisher. Two consequences for later cuts:
  • C2 covers the StreamPixels service too. apps/service/src/app.ts:155 answers route challenges through publishRouteObservation, which C1 removed, so the gitlink bump breaks the service unless C2 moves it to answerRouteObservation and reportHealth.
  • B3 surfaces the typed capacity shortfall on the route path. Today it reads as a generic "disagrees with current authority" (control_plane.rs:5190).

S3 + C2 code status, 2026-09-29 (Self). Landed on StreamPixels route/s3-c2 (8e6d891), after Soul and a fix batch:

  • vendor/CultLib is at 30ee8b9, and the web has no Odin dependency or env.
  • Both apps answer through the signer, with one health owner, driveRuntimeHealth.
  • CI now builds the vendored gitlink, not a sibling main.
  • A production-named launcher test runs runIdunnRuntime. It caught a dropped publish argument that would have broken the service's write lease.
  • Checks: 172 tests, typecheck, the web build and the release assembly. Not merged to StreamPixels main until B3 lands. Idunn cannot yet admit a web with no Odin dependency, so a deploy of main would fail readiness. Follow-up: the production fixtures are generated by applying test-data/idunn-runtime/fixture.patch to CultLib's idunn_runtime_fixture.rs. The generator should take the target, contract and dependencies as input, so no consumer patches it. Recorded: web /api/healthz is constant {ok:true}, so the route proves "HTTP answers", not store health.

C2 — StreamPixels web binding proves invariant 3 end to end (StreamPixels, after C1, S3, B3).

  • Point the gitlink at the C1 commit.
  • The Next /cultnet/snapshot handler answers from answerRouteObservation. No publisher is registered and no Odin env is read.
  • Run the web tests and build in the node image on Yggdrasil.
  • Live acceptance:
    1. Deploy the web through idunn up with Odin reachable, then repeat with Odin's UDP route broken on purpose, or with Odin stopped under the lifecycle brake.
    2. Both runs must admit.
    3. Both must show zero steady-state reloads over one hour.
    4. Both must show repeated successful signed challenges. Run 2 (Odin stopped) is blocked on B4. The failure on 2026-09-29 21:15 is the unlanded B4, not a C2 defect.
  • Public cutover is out of scope and resumes afterwards through the ordinary path.

Build and verification path

  • Idunn tests use ygg-verify.sh F:/Projects/Idunn <rev> rust '<cmd>'. The command must keep its own exit status last. Heavy builds must not run on Starfire.
  • CultLib TS runs from packages/cultnet-ts in a node image.
  • StreamPixels runs pnpm in a node image.
  • Live timeline probes (reload counts, worker counts, journal) are read-only commands on Yggdrasil, taken before and after each deploy.
  • Each Idunn cut names its focused tests. The whole suite runs once per cut on Yggdrasil, not per edit.
  • Every Idunn cut also runs cargo check --locked --target x86_64-pc-windows-gnu --bin idunn-host (W1), without -D warnings.

Ops follow-ups, out of scope

  • worker_shutdown_timeout: the probe found none configured.
  • Retention of dead transient units and activation directories.
  • Odin's UDP listener proxied through nginx: every deploy-time reload still moves Odin's flows (F7). Invariant 1 removes the steady-state reloads, not the install-time ones. Whether Odin should bind its stable endpoint directly is a later route-driver question.
  • Heimdall's repeated source and runner failures appear in idunn status, but that work is excluded.

5. Odin in Idunn's lifecycle: authority audit (Imagination, 2026-09-30)

Operator concern, 2026-09-30: "it feels like Odin is doing lifecycle work when that's very much Idunn's wheelhouse." The operator is right. Source-read at Idunn 94c7691 (src/ byte-identical to 583b2a7), Odin 5c37860 (deployed; unmerged hands/odin-session-table = eeb6812 is flagged "D"), CultLib af7209a2, gamecult-ops 7d5749f. Nothing was probed on Yggdrasil. Rulings: Q-O1 to Q-O5 in section 3; cuts in section 4.

The live seam. Idunn delegates readiness to Odin for every target that declares the Odin discovery dependency, and Odin originates ready, including a transitive dependency-freshness verdict. Idunn reads that word at Warming, lease grant, Ready, Routing, Commit, admitted refresh, provider selection, provider currency, and at its own boot. Worst consequence: a continuity restart of ghostlight or streampixels-service waits in Warming with no end until Odin speaks (I2). The dead residue: Odin's repo carries a complete keepalive/restart/deploy planner, about 30 idunn.* lifecycle schemas, a retired JS coordinator, and about 3,200 lines of deploy/restart/health actuators for other services. None is deployed; the docs still name it as Odin's authority.

What was missing. Idunn's only way to hear a signed presence is request_candidate_runtime_presence (drivers.rs:5473-5486), which needs Expected.route. A target with no route had no Idunn-owned eye, so Odin (which received every presence for discovery) was drafted as the eye (ReadinessClass), then grew the verdict (ready, Odin lib.rs:773-777), then the dependency policy (:1002-1103, :1192). Every Odin-correlated target except raven-muninn is routed, so for them Idunn already owns the channel.

Classes: 1 = observation Idunn can equally get itself; 2 = Odin relaying a provider's self-report; 3 = a legitimate Odin concern that merely looks lifecycle-ish; 4 = Odin making a lifecycle judgment it should not own. "Gate" marks a site that decides, blocks or delays an Idunn outcome.

5.1 Idunn sites (src/control_plane.rs unless named)

# Site What Odin decides Class Gate
I1 ReadinessClass::declared/of :632-670; CompiledDeploymentPlan::readiness_class deployment_plan.rs:645-661 A discovery dependency makes Odin the readiness voucher 1 (root conflation) chooses every row below
I2 advance_warming Odin branch :5597-5700, gate text :5653 Warming waits for Odin's correlation. Only OdinSelf is exempt (:5614-5618); Continuity is not exempt for anyone else 1 Warming, deploy and continuity
I3 is_semantic_warming :8098-8128, warming_disagreements_match_incumbent :8130-8145 Interprets Odin's present, state and disagreements as Warming 2 Warming
I4 fresh_warming_for_lease OdinTopology arm :6500-6544 A newer post-fence Odin sequence is required before the write lease is granted 1 lease grant
I5 advance_awaiting_ready :6050-6089 Ready is Odin's ready, which Odin originates (O1, O2) 1 + 4 Ready
I6 advance_routing :6096-6135 A non-Ready latest word is Err post-fence (:6116-6119); the deploy resumes forever until B5 1 Routing
I7 advance_committing :6220-6330 Topology admitted twice around the final proof (F16 livelock). Commit copies current_odin_authority onto the generation (:6343) 1 Commit
I8 refresh_admitted_topology :5272-5339 Supervision stores Odin's latest word, Ready or not (:5320-5325); only Ready moves ready 1 / 2 provider exclusion
I9 current_ready_provider_tokens / rehydrate_admitted_ready :7398-7470; managed_ready_provider_refs deployment_plan.rs:264-316; ManagedReady odin fields :138-148 Provider eligibility is Odin's latest word; capabilities are the provider's presence as relayed by Odin 2 + 1 plan compile (C2 run 2)
I10 validate_selected_providers_current :7472-7534, called at :5466, :6140, :6253, :6327 The same, re-checked at Starting, Routing and twice at Commit (Deploy only, :8074-8080) 1 deploy abort or resume
I11 Graph gate: every non-optional dependency needs a provider (deployment_plan.rs:447-455); the Odin dependency is shared-infrastructure Odin must be admitted and current to compile any Verse-advertising target 3 in intent, gate in effect compile
I12 Boot: Engine::open requires the bootstrap Odin anchor file (:3900-3902); validate_durable_authority re-proves every Odin receipt against the admitted Odin's key (:4110-4225); boot refuses on any failure (:4355) An Odin-owned key and anchor gate Idunn's own startup 4-by-proxy (doctrine violation) Idunn boot
I13 current_odin_authority :4041-4054; AdmittedOdinAuthority :1905-1935 Bootstrap key until Odin is admitted, then the admitted Odin's key. Odin's own commit copies the current authority (test :14920-14936), so Odin's topology key can never rotate through a deploy 3 (trust anchoring), exists only for I1 all of the above
I14 odin_publisher_sequence_cursor, max_odin_sequence :2762-2808, sequence_requires_admission :8052-8072 Idunn's anti-replay depends on Odin's persisted watermark. If Odin loses state, every Odin-correlated target gets "replayed or reordered" as Err 3, exists only for I1 Warming, refresh
I15 Topology projection: CultCacheTopologyDriver drivers.rs:4238-4250, publish_*, demote_to_expected_only Idunn publishes Expected, activation and lease; Odin and the workloads read them 3 (correct direction) none. Its doc comment ("only Odin may correlate ... into Present/Ready", drivers.rs:4239-4241) is the misplaced doctrine in prose
I16 AdoptionSource::OdinTopology :786-795 No production writer (only tests, :11502, :11692). B5's adoption slot dead variant none
I17 validate_live_providers_for_deploy :8074-8080 Continuity skips provider currency. The only continuity exemption; I2 and I4-I7 have none n/a n/a

5.2 Odin sites (crates/odin-daemon/src/lib.rs at 5c37860 unless named)

# Site Mechanism Class
O1 ready :773-777 presence.state == "active", relayed, and no disagreements, and every dependency permits Ready. Odin originates the verdict Idunn acts on (I5-I10) 2 + 4
O2 dependency_evidence :1002-1103 (ready :1098, D :1108) Authenticates the provider's stored correlation against now, 30 s window, plus capacity. A provider whose correlation is not re-stamped for 30 s flips every dependent to not-Ready: a transitive freshness verdict 4
O3 dependency_permits_ready :1192 (D :1202) Kind policy (optional and external-operator-binding never block) lives in Odin. It already bent recipes: Ghostlight c5bf090 dropped its Heimdall dependency to dodge it (gamecult-ops/runbooks/ghostlight-dungeon-yggdrasil.md:49-55) 4
O4 admit_presence :654-676, select_presence :886-897 A presence is admitted only for an incarnation Idunn projects. Odin consumes Idunn's truth 3, keep
O5 same_publisher :929-956 (D :961/:964) Anti-replay within one activation. Surfaced as a lifecycle outage only because of O1 -> I8/I9 3, keep
O6 Stored presence re-authenticated at its receipt time :960-988; 30 s max age and 5 s skew :62-71 Odin's admission window 3
O7 correlate_write_lease :1126-1157, classify_runtime_authority :1196-1324, classify_current_lease :1326-1356 Odin re-verifies Idunn's own artifacts. Redundant with Idunn 1
O8 Correlation dedupe and watermark :541-575, :776-792 Publisher discipline toward I14. Dies with the correlation 3
O9 main.rs stored_snapshot :466-490; raw_snapshot :290-310 (D main.rs:298-316) Filtered relay of provider bytes. Odin answers challenges to itself with signed_presence_document, the same signer and authority its self-publication uses 2/3, keep
O10 main.rs activation waits for Idunn's lease, 300 s (:67, :580-589) Odin as a stateful Idunn target correct direction
O11 survive (D main.rs:671, eeb6812) Every error except WriteLeaseLost is transient, including permanent self-publication failures see the temporary rule in section 4

Dead residue (class 4 in content; nothing deployed; cut by X1):

# Residue Status
O12 crates/odin-core/src/idunn.rs (672 lines), including plan_keepalive :14-173; documents.rs:28-59, about 30 idunn.* lifecycle schemas Uncalled. odin-daemon does not depend on odin-core. Muninn and Sleipnir (Muninn workspace) still emit IdunnDaemonHealthRecord via odin-core pinned at 3e96c6c; no live Idunn reads it
O13 JS coordinator src/ (2,814 lines) plus tests (140 lines): probes.cjs docker and adb probes, state.cjs mints service states, provider-ingress.cjs 120 s TTL and hard-coded active Not deployed (state/map.yaml:30). Still reachable by package.json start, scripts/start-odin.ps1, scripts/restart-odin.ps1, and gamecult-ops/compose/odin.yggdrasil.yaml
O14 scripts/ (3,208 lines): deploy, restart and health actuators for heimdall, streampixels, repixelizer, stonks, vili, weksa, nightwing and voidbot Deploy scripts dead but armed (IDUNN_ACTUATOR=1, referenced by gamecult-ops/scripts/idunn/idunn-deployment-targets.ps1:178-216). Restart scripts target the retired Windows Idunn. Health scripts are stubs or orphans
O15 docs/architecture.md :49-62, :97-109, :218-228; README.md :71-105 Claim process lifecycle, health and "Rust lifecycle logic" for Odin, and name crates/idunn-daemon, which does not exist

Recipes and ops.

  • G1: Muninn 2260853 raven-muninn.toml declares the Odin dependency to obtain a readiness class (its own comment says so). Unrouted. Held Undeclared on the live store, so it has no continuity restarts today.
  • G2: Ghostlight c5bf090 and the ops runbook :49-55: a real dependency was removed to escape O2 and O3. Odin's gate is shaping the dependency graph.
  • G3: gamecult-ops/scripts/idunn/idunn-deployment-targets.ps1:94 labels "odin-correlated-runtime-presence"; display-only.

5.3 Target authority map

The decision. Is this admitted incarnation, or this candidate, Warming, Ready, or current enough to serve a dependent? And when does its absence restart it?

  • Owner. Idunn's phase engine and supervision, acting on Idunn's own challenge of the incarnation's challenge endpoint (the candidate endpoint pre-promotion, the stable endpoint after), answered with a presence signed under the Idunn-issued launch authority; and on Idunn's own workload observation (systemd or host actuator). One proof path serves every target; there is no class.
  • Inputs. Expected, activation and lease (Idunn's own); the challenged signed presence (the provider's own words, authenticated by Idunn end to end); workload observation; brakes; B2's route state, degraded or not; TargetSupervision meters.
  • Outputs. Transaction evidence (Direct warming, Direct Ready); admitted generations; provider currency (latest proof not degraded and within 2 x max age, B4′'s rule); capabilities from the challenged presence; the topology projection (I15), unchanged, which Odin and the workloads read.
  • Derived or demoted.
    • ReadinessClass is dead. What survives is "does the binding give Idunn a challenge endpoint?", and the refusal when it does not.
    • Odin's correlation, if Odin keeps publishing anything, is display and discovery only. Idunn never reads it.
    • ODIN_RENDEZVOUS_CAPABILITY is an ordinary capability string; Idunn has no reason to name it.
    • "Is Odin" is a question Idunn never asks.
  • Forbidden writers. None of these decides any lifecycle outcome:
    • CultCacheTopologyDriver::receive and the odin_correlation_store option;
    • admit_latest_topology, refresh_admitted_topology, rehydrate_ready_token, rehydrate_warming_token's Odin arm, rehydrate_admitted_ready's Odin form;
    • is_semantic_warming and is_semantic_ready;
    • current_odin_authority, the bootstrap Odin anchor, AdmittedOdinAuthority;
    • odin_publisher_sequence_cursor, max_odin_sequence, sequence_requires_admission;
    • WarmingEvidence::{OdinTopology, FirstOdinDirect}, ReadinessEvidence::OdinCorrelated, AdoptionSource::OdinTopology;
    • boot's re-proof of Odin receipts;
    • in Odin: ready, dependency_evidence, dependency_permits_ready.
  • Shared paths. The same challenge_* primitive and authentication serve deploy and continuity Warming, the lease-grant freshness check, Ready, Routing, Commit, supervision and provider currency, and boot re-proof of Idunn's own direct evidence only. Deploy and continuity differ only in the brake they ask and in the provider-currency skip (I17).
  • Deletion line. Delete every forbidden writer above, plus the latest_odin_observation and odin_authority fields and the --odin-trust-anchor and --odin-correlation-store options, before any new currency or health behaviour is added on top.

Odin, in the target. Odin consumes Idunn's projection (O4) to know which incarnations exist and which activation to trust; admits presences for discovery with anti-replay (O5, O6); serves the catalog, routes and Eve surfaces. It may display whether a provider looks alive; nothing downstream gates on that display. Odin's own lifecycle is Idunn's (O10). Schema ownership for idunn.* documents leaves Odin.

5.4 Odin's own health (Soul input on eeb6812)

survive swallows every error except WriteLeaseLost, including three permanent self-publication failures. Idunn restarts only on workload death (control_plane.rs:4990-5050), and Q5 (a) marked a failed route proof degraded without restart. What Idunn should observe is its own stable-route challenge of the admitted Odin (supervise_admitted_route, no new signal): Odin answers through signed_presence_document, so "no runtime authority" and "signer/anchor mismatch" make the challenge fail visibly. The fail-closed store branch breaks only Odin's catalog entry for itself: a discovery defect Odin should show on its own Eve surface, not exit over. Q-O5 rules what Idunn does with an unauthenticatable answer; the interim is the temporary rule in section 4.

5.5 Open items for Soul

  • Continuity-restart stall probe (not yet run). build_routed_world with the recipe's [[dependencies]] kept (OdinCorrelated), Odin::Unreachable or an absent store, seeded at Warming as Continuity. Assert it stays in Warming and stub.candidate_hits == 0 even though the candidate would answer. Confirms I2 before A1 removes it.
  • Journal confirmation of the exclusion seen since 19:45 CEST. Ghostlight and raven-muninn have been excluded ("latest ... not Ready") on every compile with Odin up. Unconfirmed hypothesis: by O2, their Odin-computed ready is false whenever Odin's correlation about itself is more than 30 s old, and at 5c37860 Odin's self-presence crosses its own RUDP session table (the fault a9ed585, unmerged, fixes); so O2 plus the self-presence session-table bug produce the exclusion. Confirm from the dependencies[].ready of their latest correlation via Idunn status or journal text, not a store read.
  • The Idunn status and Eve rendering of Odin fields was not audited. It is display, and dies with A3.