deploy/cultlib.commit said 542ddd3 while the built pin is Cargo.lock's 3bf1c0c.
Nothing reads the file (the Idunn recipe builds with `cargo --locked`), so it is
deleted and the README names the lock. package.json's muninn:build/muninn:start
named a muninn-daemon crate that no longer exists in this workspace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Odin published its own heartbeat over RUDP to its own document server, which
holds 64 sessions for 30 s after each peer's last packet. Ghostlight,
CodexConnector and the presence publishers never send Disconnect, so a burst of
publishes filled the table, Odin's Connect was dropped, the heartbeat failed and
`main` returned. Reproduced with 70 lingering sessions: 4 of 5 self-publishes
failed.
Owner change: Odin is the store, so its heartbeat is signed and admitted through
the same admission path providers use (`admit_presence_document`), with no
socket, no thread and no session slot. The self-publish timeout and the RUDP
publisher are deleted.
Serving-loop failure policy, in one place (`survive`, `settle_poll`): only
`WriteLeaseLost` (the process-write lease withdrawn or replaced, i.e. serving on
would be a second writer) ends Odin. A failed heartbeat, a failed projection
refresh and a failed poll are logged and retried; a socket failing every poll for
30 s is a dead socket and ends Odin so Idunn restarts it. An application
rejection or stray packet never ends Odin. The startup publish/refresh pair is
folded into the loop (a timer that has not fired is due). One serving pass is
`serving_pass`, the unit the tests drive against Odin's real activated state and
production RUDP server.
Tests (Linux, real activation, lease, projection and identities): a full 64-wide
session table of lingering publishers does not stop 5 consecutive heartbeats
landing in the store; a corrupted store fails publication and refresh without
ending Odin and the next pass lands; replacing or removing the lease ends Odin;
an application rejection ends only the offending session and a stray datagram is
dropped while Odin keeps admitting peers; a failing socket is retried until the
failure limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stored presence that names the current activation but no longer authenticates
at its own received time is an error (fail-closed): its count is unknowable, so
the claim must not slip past it. A stored presence that cannot be decoded names
no activation, so it is another publisher's and the claim supersedes it
(fail-open). The choice is now written on the branch and each half has a test
that fails when it flips.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stored presence constrains an incoming claim's sequence only when it names
the current activation (activation_witness_sha256 equals the authority's
activation_sha256). Previously any re-authentication failure meant "different
publisher", so a same-activation presence that stopped authenticating silently
lost its sequence. Now such a failure is an error.
Adds a test pinning that the stored presence is re-authenticated at its own
received-at time: sequence 5 admitted at T, sequence 4 offered at T+40s from
the same activation must be refused as reordered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Odin crashed on a stray RUDP packet; CultLib 3bf1c0ce hardens stray-packet
handling in all runtimes. Both manifests (odin-core, odin-daemon; Odin has no
workspace.dependencies) move from a8aeddac to 3bf1c0ce. No API breaks: the
workspace compiles and tests unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sleipnir moved into Muninn with its history (Muninn f4f1f85, operator ruling
Q9); the StreamPixels ship that gated this deletion completed 2026-09-29.
Removes the crate, its workspace member, its Cargo.lock entries (including
vigem-client), the sleipnir:build/sleipnir:start package scripts, and the
deploy/restart/start launcher scripts that shipped Odin's sleipnir.exe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From 2026-09-29 19:20 on yggdrasil every new StreamPixels incarnation's
runtime presence was refused with "runtime presence publisher sequence
was reordered". Odin compared the incoming claim's sequence with the
target's stored presence whoever had published it; the provider counts
from 1 in each process, so once a target had published for a while no
restarted activation could ever be admitted.
A publisher is the activation whose key signs its claims. The stored
presence now constrains the incoming claim only when it still
authenticates under the same current authority. One left by an earlier
activation, or otherwise stale, is superseded whatever its sequence.
Within one activation a lower sequence is still a reorder and an equal
one with different bytes is still a reuse, and a claim that does not
authenticate under the current activation is still refused.
The unconditional decode of the stored presence at the top of
select_presence existed only to feed that comparison and is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The StreamPixels deploy archive now carries CultLib's root build files,
contracts/cultnet, cultmesh-ts and cultmesh-browser, so the host can
build CultLib's TS dist/ itself. It previously relied on a June tarball
that happened to include dist/. This was verified on a clean Debian 13
extract and gated by Soul with StreamPixels Cuts E and F.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Odin has no incarnation to observe for a provider Idunn admitted from an
operator binding, so it can never sign evidence for one, and every such
dependency stayed ready: false. A dependent with one (Ghostlight Dungeon's
Heimdall command boundary) could never become Ready, and Idunn waited in
AwaitingReady indefinitely. Idunn and CultLib already accept the kind.
The dependency's own evidence stays ready: false; only its dependent's
Ready no longer waits on it, as with optional. The dependent checks that
provider itself at use. The rule is now one named function with a test
covering managed, optional and external kinds; dropping the external kind
fails it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cultnet-ts's build (generate-swarm-contracts.mjs) mirrors CultLib's
canonical contracts/cultnet into its own package tree at build time, so
`npm run build:ts` fails on cultnet-ts on a host that only has the
packages the previous archive list carried. Verified by a dry run of the
Cut E archive on Yggdrasil: build:ts failed with ENOENT on
CultLib/contracts/cultnet until this path was added.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The archive built only packages/cultnet-ts and packages/cultcache-ts, and
dist/ is gitignored in both, so the current actuator ships CultLib
packages with no dist/ and the service fails at import (P1). Add the
CultLib root package.json/package-lock.json and
scripts/run-typescript-workspaces.mjs so the deploy host can build
CultLib's TS dist/ itself, and carry packages/cultmesh-ts and
packages/cultmesh-browser alongside the two packages already archived.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deletes crates/odin-core/examples/dump_move_tracker_health.rs and
dump_provider_endpoints.rs (no in-swarm caller: full-corpus rg for both
executable names found only their own usage strings, plus the two sibling
Odin worktrees that carry byte-identical copies). Removes package.json's
gjallar:build/gjallar:start scripts, which run `cargo build/run -p
gjallar-daemon` — no such Cargo package exists in this workspace (members:
sleipnir-daemon, odin-core, odin-daemon) or in the sibling Gjallar repo,
which is a C#/.NET executable (src/Gjallar/Gjallar.csproj), not a Rust
package.
Kill attempts that failed to find a consumer:
- rg -n "dump_move_tracker_health|dump_provider_endpoints" across all of
F:/Projects excluding target/node_modules/.git: only the examples' own
usage strings, the census docs, and the two other Odin worktrees.
- rg -n "gjallar:build|gjallar:start" across all of F:/Projects: only
Odin's own package.json (this repo + 2 worktrees) and census docs.
- Collision check: git diff --stat origin/main...codex/cultmath-cutover and
...codex/ygg-ghostlight-continuity over these paths: empty, no in-flight
work touches them.
- state/map.yaml and notes/*.md: no reference to either example or the
gjallar npm scripts.
Not touched: the root Cargo.toml exclusions for vendor/cultcache-rs and
vendor/cultnet-rs named in REC-014 are already gone — removed by commit
6575de4 ("Drop the workspace excludes for vendor trees that no longer
exist"), prior to this pass. That part of REC-014 is moot.
REC-014
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On 2026-09-11 a provider's continuity restart (raven-muninn, new
activation) left its previous presence in Odin's store. Odin treated a
stored presence that failed authentication under an authority its own
correlation still named as tampering and exited; every continuity
restart read the same record at startup and died the same way, three
times, and Idunn gave up on odin. The rendezvous was dark for the whole
Verse over one provider's evidence. It is now recorded as a
stored-presence-not-current disagreement and logged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Idunn runs the daemon as PID 1 of its own PID namespace, and a namespace init
ignores every signal it has not caught. Every stop was a ninety-second wait
for SIGKILL, which is what fencing the incumbent cost tonight's fold-back
candidate: it gave up on its sixty-second bootstrap wait before Idunn could
grant the lease. Odin now catches SIGTERM and SIGINT and returns from its
loops; the bootstrap wait is bounded at five minutes, with Idunn's abort as
the real owner of a candidate nobody leases.
Verified in rust:1.95-bookworm on yggdrasil: 22 + 13 (+1 ignored) + 4 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The store the target-keyed Odin left behind holds correlations under bare
target keys. The incarnation-keyed reader treated one as a substituted key
and the candidate exited at start (tx-0f15abe4 on yggdrasil, 21:13Z). Those
records are now skipped by the reader and retired at activation; presence
history is kept under any key so the self publisher sequence continues.
Verified in rust:1.95-bookworm on yggdrasil: 22 + 13 (+1 ignored) + 4 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The three muninn crates, their seventeen actuator scripts, the two Raven
capture-mux wrappers that called one of them, and the two Muninn docs all
leave this workspace. GameCult/Muninn has carried them since 2026-09-09 and
has moved on (media stream advertisement, receivers dialling the producer);
this copy was a fork drifting behind under the same name, and gamecult-ops
now binds every Muninn target to the Muninn repository. Odin keeps the
Muninn records in odin-core until they are moved; that is the remaining
piece of the extraction.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A target being replaced has two incarnations at once. Idunn's projection and
Odin's store keyed every record by target, so the candidate's Expected
overwrote the slot the incumbent validated itself against and the incumbent
died on its next heartbeat. Records now live under {target}@{expected sha}:
Odin looks up its own incarnation by its bundle's Expected digest, admits a
presence under the incarnation the presence names, keeps one correlation per
incarnation, and resolves dependencies by (provider, expected sha). Records
keyed by the bare target are the previous single-slot shape and project
nothing.
The negative check the rebuild contract asks for is in
a_candidate_expected_beside_the_incumbent_cannot_reject_its_presence.
Verified with cargo test -p odin-core -p odin-daemon in rust:1.95-bookworm on
yggdrasil: 22 + 12 (+1 ignored) + 4 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Live probe on yggdrasil: the queued deployment is not starved. Each time a
continuity restart completes it seals and publish_expected replaces the one
Expected slot for odin with the candidate's; the incumbent's next heartbeat
fails activation-expected-projection against its own catalog and it exits.
notes/odin-cycle-mechanism-2026-09-10.md carries the timeline, snapshots'
decode, the authority map, and the fork to decide. projection_authority_probe
is the ignored dev test that decoded the captured projection versions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2265184 claims to be a deploy-script fix and is also 70 lines adding an
ignored `projection_authority_probe` test to crates/odin-daemon/src/lib.rs.
That probe was uncommitted in this working tree and `git add -A` swept it
in under a message that says nothing about it.
Recording rather than rewriting, per this repo's ledger convention: main
is shared and force-pushing it to tidy a message is worse than a message
that needed a correction.
The probe itself is sound and worth keeping -- it decodes a captured
projection and prints what Odin's authority classifier sees for one
target, which is the diagnostic the current cycling incident wants. It is
`#[ignore]` and env-driven, so it changes nothing unless invoked.
Verified green after the fact: cargo test -p odin-daemon ->
10 passed; 0 failed; 1 ignored.
Third time this session that `git add -A` in a filesystem shared with
other agent sessions has committed work that was not mine. `git status`
before `git commit`, every time, is the cheap habit that would have caught
all three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultLib split packages/cultcache-py into cultcache-py, cultnet-py and
cultmesh-py. Module names did not change, so archiving only the first
still succeeds and produces a tarball missing two thirds of the library --
a failure that surfaces later as an ImportError with nothing pointing back
to this line.
The two sibling deploy scripts ship cultnet-ts and cultcache-ts and are
unaffected by a Python split.
Not exercised: repixelizer.service is inactive, so this is a trap defused
rather than a break repaired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified on yggdrasil 2026-09-10T19:23Z: units exit after 2.5-16 min, not
~96 s; that number is the RUDP ack timeout. Unit hashes are per-activation,
not the release hash. Unprivileged journalctl returns a false zero.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AGENTS.md named state/map.yaml as the current deployment and admission
map; leaving it absent would have been a dangling reference in the file
that tells a cold session where to look.
Carries the decoy odin.service unit -- active, MainPID 0, owning no
process -- and the armed queued deployment, because both are things a
reader will otherwise discover the hard way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Odin had no AGENTS.md, no CLAUDE.md and no notes/ or state/, so every
session started cold in the repo whose deployment is the least routine in
the estate.
AGENTS.md carries what is specific to Odin rather than repeating estate
doctrine: that the host is authoritative, that a local compile says
nothing about the Linux release, that /etc/machine-id must be mounted or
identity tests fail in a way that looks like broken code, and the two
listing traps that have each misled a reading -- an alphabetically
truncated unit list, and odin.service being active while owning no
process.
notes/odin-deployment-handoff-2026-09-10.md records why Odin currently
cycles and why the two obvious interventions fail: an engaged lifecycle
brake parks a continuity transaction on the target and blocks deployment,
which the operator doctrine says it must not; and freeze_one_queued_command
only runs when continuity made no progress, so a queued deployment is
starved on exactly the unstable target it exists to repair.
Both defects are named with the state that proves them rather than left
as a description of a bad night.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0e540ca moved the rev in four Cargo.toml files and left the lock naming
c13b6ba. Idunn's build runner passes --locked, precisely so a sealed
source builds the dependency set it was sealed with, and refused:
error: cannot update the lock file /workspace/Cargo.lock because
--locked was passed to prevent this
Which is the runner doing its job. The failure landed in Sealing, before
fencing, so the admitted incumbent was never touched -- Odin stayed up on
the same pid throughout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cultnet-rs removed a fragment buffer only on successful reassembly and
refused new fragmented payloads once 64 sets were stranded, so any
sustained loss on a fragmenting channel ended with the receiver returning
"RUDP pending fragment-set limit reached" for every subsequent fragmented
payload. Odin is the rendezvous every other target is observed through;
a receiver that stops accepting fragmented traffic after a bounded number
of losses is the wrong failure mode for that role.
Measured on Raven at 3% loss, 12 Mbps: the old receiver dies at 3 and 6
fragments per payload, the new one delivers 91.0% and 82.7%, and the
loss amplification matches 1-(1-p)^n exactly.
The pin also carries the media stream contracts and the media delivery
option, neither of which Odin uses, and the TypeScript and Python halves
of the same eviction fix.
All 11 CultLib revs across odin-core, odin-daemon, muninn-daemon and
sleipnir-daemon move together; leaving any behind gives two cultcache-rs
copies and a DatabaseEntry that is not DatabaseEntry.
cargo test -p odin-core -p odin-daemon -> 22 passed, 10 passed, 4 passed,
0 failed
muninn-psmoveapi-tracker still fails to link in the build image for want
of libpsmoveapi. It declares no CultLib dependency, so the pin cannot
reach it; that is a missing native library on the builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
reconcile reserved a sequence from the watermark before comparing the
new correlation against the prior one, so every refresh -- Odin's idle
timer, a restart, a dependency re-read -- advanced the mark and fsynced
topology.cc while returning the prior bytes unchanged. The sequence Idunn
receives then jumps by however long Odin sat idle rather than by the
number of observations that actually differed, which is the opposite of
what a monotonic publisher sequence is for.
same_correlation_facts normalizes publisher_sequence to the prior's
before comparing, so the reserve has no bearing on the dedupe decision
and moves below it wholesale.
Verified against the old ordering: three idle refreshes then one real
observation yields sequence 5 instead of 2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Idunn publishes an activation only after observing the process that owns it,
and every launch -- deployment or continuity restart -- is issued a fresh one.
So at startup the projection either carries no activation yet or still names
the incarnation being replaced. Neither is this process's, and both are normal.
Requiring one broke the first start of a target. Requiring a match broke every
start after it, which between them is every start there is: Odin could be
deployed exactly once, and any restart or redeploy left it refusing to start
while continuity restarted it into the same wall.
Nothing is taken on trust for dropping it. The bundle's own activation is
verified against the Idunn anchor, and the write lease -- the thing that
actually authorises writing state -- is still checked against the live
projection before Odin writes anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sequence was derived from the stored correlation, so withdrawing one took
the count with it and the next started at 1. Idunn remembers the highest
sequence it has admitted per target and refuses anything at or below it, so
every later correlation was refused as a replay -- and since that refusal is
what withdraws the correlation, the count never climbed past the old mark. A
deadlock that tightened with each attempt, and it cost Heimdall every
deployment after its first two.
It was also the exact weakness the check exists to prevent: while the count
was back at 1, a replayed correlation carrying the old sequence 2 outranked
the genuine one.
The mark now lives in its own record, which withdrawal does not remove, and is
reserved before the correlation that uses it is published -- so a crash in
between leaves it ahead. Sequences may skip; they may never repeat. A stored
correlation ahead of the mark still wins, which carries an existing store onto
the new scheme without a migration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Odin's startup demanded that the live Idunn projection already carry the
runtime activation matching its bundle. Idunn publishes that record only
after it has observed the started process -- the activation carries that
process's executable digest -- so at the moment Odin checks, it cannot exist.
Every first start failed with "Idunn live projection differs from Odin's
immutable runtime bundle".
The Expected incarnation is still required to match, and a projected
activation, once present, must be this one, so a stale or foreign activation
is still refused. The bundle's own activation is verified against the Idunn
anchor regardless, and the write lease is checked against the live projection
before Odin writes anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Idunn launches Odin with PrivatePIDs=yes -- the private PID namespace is part
of the isolation it proves between a candidate and the incumbent -- and
systemd sets LISTEN_PID to the pid it knows, which is in the outer namespace.
Inside, /proc/self/status reports only the namespace-local pid (measured: pid
3, LISTEN_PID 2994180). The value does not merely differ, it names something
this process cannot observe, so the equality could never hold and every
candidate exited immediately with "systemd signer descriptors belong to
another process".
The protocol check exists so a child does not mistake inherited LISTEN_FDS
for its own. What remains is stronger: the exact descriptor count and name
order, and then the content, which must verify against the trust anchor Idunn
published. Descriptors that were not Idunn's fail that verification.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only a step's declared `required_environment` is forwarded to the runner
container. Odin's steps declared none, so CARGO_HOME and HOME never arrived
and cargo fell back to the image's read-only /usr/local/cargo, failing to
fetch the rev-pinned CultLib dependency.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Odin's recipe declared `[state]` with a `topology` slot but its launch
contract passed `--store` as an absolute path. Idunn refuses that pairing:
a workload whose recipe declares state must bind a state root, and binding a
state root requires the launch contract to carry a `state_root` argument. No
operator binding for Odin could have been admitted. This never surfaced
because `idunn validate` was run recipe-only, so `admit` never saw it.
The rule is Idunn's on purpose -- it is what makes Idunn the owner of where a
target's state lives, so it can create and harden the root. Odin naming its
own absolute store path routed around that: a binding could have placed the
store outside the root Idunn prepared.
So the daemon now takes `--state-root` and derives the store from the slot
the recipe already declares. The store path is no longer supplied by anyone;
`--store` is rejected, and a test pins that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Static lowering landed in GameCult/Hermodr: a library and CLI rendering a
surface graph to standalone HTML through the same renderer the browser uses,
with command controls inert because a static page has no back-channel.
All five phases of the Hermodr extraction are complete. This document stays as
the record of what left Odin and why.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>