Two rules had no test of their own (Soul's mutants survived):
- only_oldest_attempt_tried: every fixture's file held the oldest failed
attempt. The new test leaves the file holding a later one.
- remove_not_dirty: the heartbeat always dirtied the store, so a removal
that did not mark it survived. The new test removes a record with nothing
else changed and checks the write drops it (and that removing what is not
held writes nothing).
Soul S-8 and the attempted-list gap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catalog refused itself whole ("duplicate public document identities")
whenever two projected incarnations of one target both held an admitted
presence, which every provider has during a deploy window: presences are
served under (schema, target), and the incumbent and the candidate share it.
One is now served and the other skipped, logged once per interval: the
incarnation whose Odin correlation is Ready, and between two that are both
or neither Ready, the later observation. A duplicate identity between
anything but two presences still refuses the query.
Tests: the incumbent (Ready) is served over a newer Warming candidate; with
no correlations to decide, the later presence is served, from either side.
Soul S-4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul's mutant main_limit_halved survived: main passed the server's
max_snapshot_response_bytes into RuntimeState::open by hand, and the tests
built RuntimeState and the server separately, so nothing tied the two. One
function, serve_with, now builds the server from its options and gives the
state that same limit; main and the test fixture both go through it. The
state has no limit until then, and Odin does not activate without one (the
fixture checks it refuses before serve_with).
Soul S-6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The small document asked for a 200-byte response, less than the response's
own fixed overhead, so sizing it underflowed. It now asks for 2,000 bytes,
inside the range where the response grows one byte per payload byte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CultNet refuses an empty message id, so sizing a document's snapshot
response with one failed for every record, and the failure was read as
"keep": nothing was ever dropped. The response is now sized with a one-byte
message id, the shortest CultNet admits, and a record that cannot be sized
is logged and kept rather than silently kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second fault in the test (removing the projection's lock file) did not
make the read fail, so it asserted a fallback that never ran. The test keeps
the fault that does: a projection file that does not decode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Odin's store carried two ~1.456 MB Eve records that no snapshot response
could ever return (the server sends at most max_snapshot_response_bytes,
1 MiB), and every write rewrote them. Under Q3 B every put writes the
store, so they cost a ~3 MB write per put.
At activation, each stored peer document is sized as the server would
answer a snapshot of it alone: the same encoder, the shortest message id.
One larger than the server's limit is dropped from the working set and
logged by type, key and size; the first write removes it from the file.
The limit is the server's own option, handed to RuntimeState where main
builds the server, so the two cannot disagree. Presences and records the
catalog does not serve are not checked.
Q1 A (refuse the unservable), Q2 (the two records are dead residue). The
CultLib half, refusing such a put at the server, is its own cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An acknowledged put is always on disk. A put refused because its write
failed may still become durable and be served: it stays in the working set,
and the next write lands it, or the failed write had already replaced the
file. Puts are latest-value per type and key, so that is sound; the
promise is written down at accept_raw_document.
Soul finding 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins the rule that only an attempted write starts the flush interval: a due
write with nothing to write leaves the next change to land on the first pass
after it. Kills the mutant that starts the interval on every due pass.
Soul finding 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reading Idunn's projection once per catalog query (620efb8) made a
projection file that cannot be read refuse the whole catalog, where before
it failed each presence lookup and left the peer documents served. The
query now answers as if Idunn projected nothing, logged once per interval:
peer documents are served and presences skipped. Odin's own projection
check (require_own_projection) is unchanged and still retries or ends Odin
as before.
The new test restores file-level coverage: a projection that does not
decode, and one whose lock cannot be opened.
Soul finding 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A write that fails does not say whether it replaced the file, so the file
may hold what any failed write since the last success tried to write. The
store kept only the last attempt: a failure after the replace followed by a
failure before it left the file holding an attempt Odin no longer
recognised, and the next write called Odin's own file foreign and ended
it. Every attempt since the last successful write is now an accepted
baseline, cleared when a write lands.
Soul finding 1, with Soul's probe as the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From Soul's SIGTERM probe (scratch 5c379dd), made a test. It runs the built
odin-daemon under an Idunn-shaped world with the warming probe and lease
grant made over RUDP. With puts written before they are acknowledged, the
SIGKILL round is the rule's test; the SIGTERM case pins `main`'s signal
wiring, which no in-process test reaches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling Q3 B: `accept_raw_document` returns Ok, and so the server
acknowledges the put, only once the store holding it is on disk. The write
is the one `flush`, under the same lease check, so it also carries every
other unwritten change. A write that is not made refuses the put. Odin's own
bookkeeping stays on the one-second interval.
The interval tests now change Odin's own bookkeeping (its heartbeat) instead
of putting, since a put is no longer an interval change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loop is `serve`, and it ends in one place: a stop request, a condition
`survive` returns, or a dead socket all reach `stop`, whose write is made
only while the lease is current and the file is still Odin's. Tested for
each exit kind: a stop request, a refused own authority and a dead socket
write what changed; a lost lease and a foreign write write nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The records stay dirty until a write lands, so the next write makes the
failed change without another mutation (tested with no heartbeat to
re-dirty them). A write that fails after it replaced the file (the
directory sync) leaves Odin's own records there; they are remembered as
the write's attempt and accepted as a second baseline until a write lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file-backed projection source no longer answers lookups itself, so no
caller can read the file once per lookup: every lookup is made against one
snapshot. The catalog query takes one per query, as the refresh pass does;
startup, activation and the own-projection check take one each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every launch is issued a fresh activation (Idunn control_plane.rs, the
continuity demotion), presences are ordered only within one activation
(select_presence), and Idunn compares only the warming presence's digest.
Continuing from a stored sequence protected nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cargo-mutants survivors, both real gaps:
- serving_pass with the timer condition inverted wrote every change on the
next pass after a write; the interval test set the timer by hand on every
pass and never saw the loop set it. A change right after a write now waits.
- correlated_incarnations returning nothing left a correlation whose
incarnation Idunn stopped projecting in place forever; the lib test
refreshed that incarnation by name. The refresh pass now has to find it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flush step is due on every pass until something changes. Hand mutant M5b
(the daemon skipping its own dirty check) left every write rule intact but read
the lease file on every 2 ms pass; no test saw it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut 1 left it imported at the top of main.rs, where only the tests use it;
the release build warned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every admission, put and refresh that changed something rewrote and fsynced
the whole store; a heartbeat did it twice (watermark, then correlation).
Now the serving loop writes the file at most once per FLUSH_INTERVAL (1 s,
operator default Q3), and only when the working set changed.
Deleted first: the per-mutation writes Cut 1 left in accept_raw_document,
publish_self_presence and refresh_all_correlations. The watermark's own write
went with the file store in Cut 1; its mark now travels in the same atomic
write as the correlations that use it. "Published" means written.
Added: FLUSH_INTERVAL, the flush step in serving_pass, and a final write on
SIGTERM/SIGINT. The dirty flag lives in MemoryOdinTopologyStore. RuntimeState::
flush is the only write of the file: it checks the lease immediately before
writing, and CultLib's snapshot compare-exchange against what Odin last loaded
or wrote refuses a file another process wrote (ForeignStoreWrite). survive
treats ForeignStoreWrite like WriteLeaseLost: both end Odin. An attempt, failed
or not, starts the interval; an interval with nothing to write does not.
Tests: 50 changes inside one interval are one write, not made at 0.9 s and
made at 1.1 s after the last; nothing changed is no write; a lease lost before
the write writes nothing and ends Odin; a foreign write ends Odin and is not
written over; stopping writes only under the lease; a crash and reload never
republishes a written sequence, withdrawal included; a flush writes only
changes and never over a foreign file. The failed-publication test becomes a
failed-write test: the store is no longer read by publication.
Map: docs/write-pattern-cut.md, Cut 2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Odin read and decoded its whole 3.24 MB store file about 30 times a second:
every refresh pass, every admission, every catalog query and every CAS retry
read it again. The working set now lives in memory under the write lease.
Deleted: CultCacheOdinTopologyStore (every file read and CAS against the
store, and its four CAS_ATTEMPTS retry loops, including the one in
reconcile), persist_generic_document's file CAS loop, the store half of
incarnation_keys, stored_snapshot's file read, and
prior_self_publisher_sequence's file read in RuntimeState::open. With one
writer under the lease an in-memory exchange cannot lose, so nothing retries.
Added: MemoryOdinTopologyStore, keyed (type, key), implementing the
OdinTopologyStore trait (still the mock point). It is loaded once, with the
locked pull_all, in try_activate after the lease is verified; legacy
retirement and the self publisher sequence are operations on the loaded set.
The trait's compare_exchange collapses into commit. Refresh reads Idunn's
projection once per pass (IdunnProjectionSnapshot) instead of once per
incarnation; admission reads it once per admission.
Not in this cut: the write policy. Each mutating operation still writes the
whole file immediately, now from memory (flush after an admission, a put or a
refresh pass that changed something).
Tests: the two tests that corrupted the store while Odin served now put the
bad record in the file at activation. New: rewriting the file behind Odin
changes nothing it serves; refresh passes and catalog reads succeed with the
file replaced by bytes that do not decode; a loaded store never reads its
file again.
Map: docs/write-pattern-cut.md, Cut 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A record that does not decode is skipped per record (catalog, startup).
Every failure of Odin's own authority ends it (temporary, until Idunn
cut A3); an early clock and a real file-read error are retried. The
catalog no longer serves correlations or the watermark. Peer puts are an
evidence-based allow-list, closing a peer-reachable presence freeze.
Three Soul passes; deploy gate GO against the live type inventory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The allow-list drops nine guessed namespaces and gains gamecult.model,
gamecult.aetheria and the two gamecult.media_stream_* types (they are not
under a dotted gamecult.media segment, so they are whole types), each with its
publisher named. The own-projection failure rule now retries only what the
file read itself marked, so a truncated Expected record ends Odin.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Peer writes (Soul P-B, confirmed): persist_generic_document blocked six
authority types, so a peer could put an odin.topology_publisher_watermark keyed
odin holding the largest sequence; every heartbeat then failed to reserve its
sequence, and the record survived restarts. Generic puts are now an allow-list
of provider namespaces (PEER_DOCUMENT_NAMESPACES). Odin's own records and
Idunn's projected authority live in namespaces no provider can name, so they
are refused by construction. The same predicate decides what the catalog
serves back, replacing the correlation special case: the watermark, and any
future Odin-internal type, is no longer public.
Own projection: a plain I/O error reading Odin's own projection (permission
denied, not found, interrupted) or a missing file is survived and retried; an
error the reader decoded or validated and did not accept stays fatal
(PresenceAuthorityRefused).
Log flood: survive() now logs through the RuntimeState LogGate, once per
subject per minute, instead of printing every pass.
Flake: self_presence_lands_while_lingering_publishers_hold_every_session failed
on Yggdrasil because 64 peers connect over about 9 s and the fsync-bound passes
that follow took 4 to 20 s under disk pressure, so the earliest sessions passed
the server's default 30 s idle timeout and expired before the final
session_count assertion (44 of 64, measured). No peer ever failed to connect.
The test's world now serves with a one hour idle timeout, since the property is
a full table at heartbeat time, and it logs any peer that is not admitted.
Tests: provider puts of every Odin-owned kind are refused and presence keeps
advancing; the catalog serves only presence and provider documents; namespaces
match whole segments; I/O versus decode of the own projection, and a missing
projection file through the serving loop; repeated failures are counted, not
printed again.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Removing the incoming-claim condition from the clock exemption in reconcile
survived: only the admission side was pinned. A refresh has no claim to refuse,
so with the clock before the anchor's binding it must still write the
correlation carrying the provider-trust-anchor-time disagreement.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A peer that times out on a loaded host no longer changes the count, and the six refused peers no longer wait out their deadline while the first sessions idle out.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A1 the catalog and startup skip a record that does not decode (logged); startup
reads only presences keyed under Odin's own target.
A2 every failure to establish Odin's own projected authority is
PresenceAuthorityRefused (temporary rule in survive).
A3 a clock that precedes the provider anchor's binding is retried, not refused.
C the catalog no longer serves correlations; the store still writes them.
B test peers connect while the server is polled without timers; 2 s deadline.
6 repeating refresh and catalog-skip lines are logged once per minute per subject.
Soul's probes P1-P3 become assertion tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
P1 two incarnations of one target, P2 one undecodable presence, P3 an
undecodable own provider anchor. All three fail at c34d167; the fixes follow.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Odin admits its own presence straight into its store instead of over
RUDP, survives transient failures, and ends only on write-lease loss or
a refused own authority (temporary, until Idunn cut A3). Merged to stop
a live crash loop on Yggdrasil (2026-09-30 00:55 CEST): the deployed
Odin timed out publishing to itself through a full session table and
Idunn stopped restarting it after three failures. Open Soul findings
(A1 startup decode, A2 rule gaps, C catalog collision, flaky harness
test) are pre-existing in the deployed build and are fixed next.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
odin-core idunn.rs and idunn.*/bifrost schemas, the retired JS
coordinator, dead deploy/restart/health scripts, lifecycle claims in
docs. Soul: nothing deleted is reached by the deployed odin-daemon, the
Hermodr container or any live store. Idunn map cut X1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README and architecture.md called Idunn "Odin's keepalive organ", listed
crates/idunn-daemon (not in this repo) and the deleted odin-core idunn.rs
as its body, described the Node coordinator's process lifecycle, refresh
loop and Docker/ADB probes as Odin's runtime body, and pointed at the
deleted start-odin.ps1. Rewritten to the live truth: Idunn builds, activates,
restarts and brakes every target, Odin included; odin-daemon reads Idunn's
projection, admits presences, publishes correlation records and answers RUDP
requests; process lifecycle is not Odin's.
The dated ledger (transport-shortcut-inventory.md), notes/ and the Hermodr
extraction plan are history and are left as written.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
start (node src/odin-coordinator.cjs), check and test (JS gates over the
deleted src/ and test/), muninn:build and muninn:start (muninn-daemon is
not in this workspace). gjallar:persona and rust:test stay. This belonged
in the Node coordinator deletion commit and was left unstaged there.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The previous commit dropped cultnet-rs from odin-core's manifest as
unused; cultmesh_documents! emits cultnet_rs paths, so the crate needs it
in scope. Restore the dependency and its lockfile entry.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Odin does not deploy, restart or health-probe anything: Idunn owns
deployment and daemon survival, and every service in this set is either
Idunn-deployed from its own repo recipe or archived. Removed from
scripts/: deploy-yggdrasil-{heimdall,repixelizer,source-app,streampixels}
(.ps1/.cmd, armed behind IDUNN_ACTUATOR=1) and the linux heimdall deploy;
restart-* (odin, stonks, vili, weksa, nightwing eve dashboard and browser
reference; several already exit 2 or throw as archived); health-* (odin,
voidbot, stonks, vili, weksa, mimir, nightwing, raven capture mux,
yggdrasil heimdall/repixelizer/streampixels/source-app; archived stubs and
orphans); start-odin.ps1 and stop-odin.ps1 (launched the deleted Node
coordinator).
Kept: write-gjallar-persona-cc.mjs (persona tooling, package.json
gjallar scripts) and the nightwing usb-move helpers, wasapi loopback,
firewall and wireguard scripts, none of which is a lifecycle actuator.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
src/odin-coordinator.cjs and src/odin/* (probes.cjs docker/adb host
probes, state.cjs, provider-ingress.cjs with its 120 s TTL) were retired
when the Rust odin-daemon became the deployed body (state/map.yaml: the
Node coordinator that held 17871 is retired). The recipe builds only
odin-daemon; nothing in the Rust crate graph loads a .cjs file. The live
Yggdrasil hermodr container runs from the immutable release
/srv/odin/current, not from this tree.
Removes src/, test/ (4 test files for that JS), scripts/check-syntax.cjs
(the JS syntax gate), and the package.json scripts start, check, test,
muninn:build, muninn:start (the last two named a crate that is not in the
workspace). gjallar:persona and rust:test stay.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
crates/odin-core/src/idunn.rs (plan_keepalive: observe, alarm, deploy or
restart from IdunnDaemonHealthRecord) had no caller: odin-daemon does not
depend on odin-core, and the deployed recipe builds only odin-daemon.
Idunn owns deployment and daemon survival; nothing in Idunn reads these
records (its own live in cultnet-rs).
Also removes from documents.rs the idunn.* and bifrost release-authority
schema constants and records, their OdinDocuments registrations, the
cultnet re-exports of Idunn health types, and the tests that only
exercised them. The muninn.* copies stay. cultnet-rs and rmp-serde drop
out of odin-core's manifest and lockfile entry (no remaining user).
Muninn pins odin-core at 3e96c6c and is unaffected until its next bump,
which must drop IdunnDaemonHealthRecord.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Refresh reads only incarnation keys and refreshes each incarnation on its own;
a record that will not decode is that incarnation's logged failure. The
CultCache projection source decodes only the requested incarnation's records.
Catalog reads serve the stored projection instead of refreshing everything.
An unreadable process-write lease maps to WriteLeaseLost. Temporary named
rule: a self-presence Odin's authority refuses (PresenceAuthorityRefused) is
fatal in survive until Idunn proves Odin by its own route challenge.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>