The portability workflow has been red since 2026-08-22. Three things,
each hidden behind the one before it:
- Eve started refusing to build a command intent for a surface that has
not advertised a command boundary and receipt schema (Eve 4338a42,
2026-08-22). The two-runtime sample never advertised one. It now passes
a provider advertisement with worldInteraction for its surface, which is
the declaration the lowering was right to demand.
- The sample and the package-closure script still imported "cultcache-ts";
the package became @gamecult/cultcache-ts on 2026-09-04 (8cb3b72) and
CI never got far enough to say so. Two READMEs carried the same import.
- The job pinned Eve to codex/eve-browser-host-isolation, a branch that was
never merged and stopped moving on 2026-08-25. The sample is written
against Eve main and passes there; the pin now says so.
The interop workflow's Python step ran one test file by path; it now
discovers the three split suites.
scripts/verify-eve-two-runtime-sample.ps1 -EveRoot F:\Projects\Eve
(Eve main ccc3ffe) -> browserCount 1, headlessCount 1, restartCount 1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
setuptools >= 77 rejects "License :: OSI Approved :: MIT License" once a
PEP 639 license expression is set, and `pip install -e` builds with the
newest setuptools. That has failed both CI workflows at the install step
since 2026-09-04. `license = "MIT"` is the expression; the classifier
said the same thing and is gone from all three Python manifests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One distribution named cultcache-py carried three libraries. Rust and
TypeScript layer them as cultcache < cultnet < cultmesh; Python now does
the same, one package per layer, module names unchanged so every
`from cultmesh_py import ...` keeps working.
Two imports ran against that layering and are resolved by moving code to
where the dependency direction says it lives, not by adding a shim:
- cultnet_py.interop_peer imported the Verse/peer catalog, authority
lease, stream, and simulation-fact contracts from cultmesh_py. They are
wire contracts a bare peer has to speak without owning a node, which is
why cultnet-rs and cultnet-ts keep them in the net layer. They now live
in cultnet_py.cultmesh_contracts; cultmesh_py re-exports them.
- cultcache_py.benchmark imported cultnet_py. It measures framed CultNet
parse beside cache paths, so it and compare_csharp move to cultnet_py.
verify checks all three packages and moves to cultmesh_py.
The single 6,693-line test module is split by what each test touches: a
test using any cultmesh_py name lands in cultmesh-py, else cultnet_py in
cultnet-py, else cultcache-py. Each suite runs with only its layer and the
layers below on the path, so a test that reaches up fails to import
rather than quietly binding the packages back together. 11 / 53 / 78 =
the same 142.
Workflows install all three; the publish job is one matrix over the
three packages keyed by tag prefix. The TS test harnesses put all three
src directories on PYTHONPATH. cultcache-py bumps to 0.2.0 because it no
longer contains cultnet_py or cultmesh_py; the new packages start at
0.1.0. Nothing was ever published, so no PyPI consumer moves.
cultcache-py 11 tests, path: cultcache-py/src
cultnet-py 53 tests, path: + cultnet-py/src
cultmesh-py 78 tests, path: + cultmesh-py/src
python -m cultmesh_py.verify -> ok, all five checks
cultnet_py.interop_peer imports with cultmesh_py absent
Not run here: the cultnet-ts interop ring and cultcache-ts store matrix
need dotnet/kotlin peers; their PYTHONPATH change is four lines.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A seventh argument sets max_fragment_bytes on the sender. Without it a
3,392-byte payload leaves loopback as one datagram, and a matrix that
claims to measure fragmentation measures nothing: the impair proxy's
received count equalled the payload count in every cell.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A receiver that survives loss by discarding stranded sets has to say so, or
the drop is invisible and the next diagnosis starts from "delivered less
than sent" again. CultNetTransportStats now carries fragment_sets_evicted,
summed across hub peers like reliable_packets_expired, and the media
delivery probe prints it beside delivered_pct.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A fragment set that lost one fragment was never reclaimed: the buffer map
only shrank on successful reassembly or a payload-size violation. Once 64
sets were stranded, every new fragmented payload returned an error and the
receiver's loop terminated. Any nonzero loss on any fragmenting channel was
therefore a countdown, not a threshold: 3 fragments at 3% loss killed the
receiver at ~700 payloads, 1% at ~2,100. Found by the media load test.
When the bound is hit, evict the set untouched longest and count it via
fragment_sets_evicted(). A stranded set's siblings were delivered long ago;
dropping it is the right call and it was already lost.
Also stop saturating the fragment id. saturating_add parked every set after
the 65,535th on one shared id, and the reassembler stitched them together.
That is the July 2026 eight-minute failure; the id now wraps to 1.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cultcache-py removed a fragment buffer only on successful reassembly, the
same shape cultnet-ts had: a set that loses one fragment is kept for the
life of the session and the map grows under any loss on a fragmenting
channel. Cap at 64 to match the other runtimes, evict the oldest stranded
set (dict insertion order) rather than refuse the payload, and expose
fragment_sets_evicted so the drop is observable.
The fragment id already wrapped correctly here; only the Rust allocator
saturated.
PYTHONPATH=src python -m unittest tests.test_cultcache -> 142 run, 1
pre-existing error (interop fixture path is cwd-relative, same on main)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cultnet-ts removed a fragment buffer only on successful reassembly, with
no cap, no eviction and no timeout. A fragment set that loses one
fragment was stranded for the life of the session, so the map grew
without bound under any loss on a fragmenting channel. cultnet-rs at
least fails closed at 64 pending sets; TypeScript just consumes memory.
This is live, not theoretical. Ghostlight sends Heimdall sealed envelopes
over CultMesh RUDP with max_fragment_bytes = 2048
(Ghostlight/crates/ghostlight-dungeon/src/heimdall.rs:697), and Heimdall's
private command plane reassembles them through this path. Every lost
fragment on that link left a buffer nothing would ever collect, on a
long-running auth service.
Caps pending sets at 64 to match cultnet-rs's max_pending_fragment_sets,
and evicts the oldest stranded set rather than refusing the payload:
a receiver that stops accepting fragmented traffic after a bounded number
of losses is a denial of service delivered by the network. Map iteration
is insertion-ordered, so the first key is the oldest. fragmentSetsEvicted
and pendingFragmentSetCount make it observable.
Found by the Muninn media load test, which hit the Rust half of the same
defect; cultcache-py has the same unbounded map and is not touched here.
npm run test --workspace=packages/cultnet-ts -> pass 54, fail 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moving the envelope moved less than it should have. Muninn's decoder validated
every record and checked the envelope's key against the key derived from the
record's own contents; the version that landed here did neither, so the move was
quietly a downgrade. Six of Muninn's tests failed on the switch and said so,
which is the only reason this is a paragraph rather than a bug.
The four record validators and the video chunk key move here with it. They check
the contract, not one producer's use of it: non-empty identifiers, non-zero
timebases, sane pressure figures, well-formed chunk keys. Left in Muninn they
would have protected the producer and left every consumer to invent its own idea
of a well-formed record — which is how a C++ receiver and a Rust sender came to
disagree while each looked correct alone.
The record-key check is the sharper one. A key is derived from the record it
addresses, so a mismatch means envelope and payload disagree about what the
thing is. Admitting that would let a consumer address media by a key its
producer never meant.
VideoChunkKey comes too. Its string form ("42:1") travels in receiver feedback
and is parsed at the other end, so it was already contract living in a producer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both branches are the same body of work seen from two angles: what a media
stream is, and what its channel does. Muninn needs both to build, so they merge
here rather than making a consumer track two refs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A media record travels wrapped in cultnet.document_put_raw.v0 with a schema id,
a record key and provenance. Both ends have to agree on that exactly, and until
now only Muninn had it: the producer was Rust in one repository and the consumer
was 6,515 lines of C++ in another, each with its own reading of the same bytes.
Ratatoskr could not decode a real stream without it.
Two things change in the move rather than after it.
The producer namespace is now the caller's. The Muninn implementation hardcoded
"muninn-media:" into every message id and "muninn.media" into every tag, so a
second producer would have had to label its traffic as Muninn's to be understood.
`MediaWireProvenance` carries it instead, and a test asserts the envelope names
no producer the caller did not supply.
The four hand-written wire payload structs are gone. Each restated a record's
entire field list as a positional tuple, in order, kept in step by discipline
alone — and existed for one reason: `#[serde(with = "serde_bytes")]` on the
payload, which cannot be applied to a DatabaseEntry struct because the derive
does not read serde attributes. Now that the records carry
`#[cultcache(key = N, bytes)]` the derive emits bin itself, so the records
serialize directly.
That last part is a wire claim, so it is tested rather than asserted:
`the_derive_reproduces_the_hand_written_wire_payload_byte_for_byte` reconstructs
the legacy struct and compares output byte for byte. It matches, which makes the
deletion a simplification instead of a silent break.
Record key shapes move here too. A consumer parses them to tell a chunk from its
siblings and a retransmission from a new frame, so they are contract and belong
beside the records rather than in whichever producer wrote them first.
Muninn's copies are not removed yet; that is the other half of this move.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three byte-exact re-encode guards exist across three separate Idunn
stores -- the runtime projection, the deployment brake, and the trust
bindings -- each decoding a record, re-encoding it, and refusing on any
byte difference. Idunn's control store does the same for its own
transactions. Three independent instances is a house pattern, and it
means changing how any persisted CultCache record serializes is never a
flag flip.
Records the measured behaviour of #[cultcache(key = N, bytes)] rather
than the assumed one: reading stays compatible both ways, so the cost
lands on reproduction and therefore on those guards, not on
compatibility. A field with no live producer is free; a field with
persisted records needs its store rewritten in the same change.
Also records that Option<Vec<u8>> cannot take the attribute today, and
that a shape-validated field nobody produces may be a fossil rather than
an omission -- the estate has at least one other case of a live consumer
outliving its producer.
This was learned by nearly deploying an attribute flip that would have
made Idunn refuse the projection store Odin reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`media` was hardcoded Reliable in two places — the profile builder and the
per-channel send options — so no caller could ask for anything else.
Measured on Raven through `cultnet-impair`, 848-byte packets at 12 Mbps, which
is Muninn's real media shape:
profile mode delivered ack bytes
clean unreliable 100.00% 43
clean reliable 69.10% 131,322
loss 1% unreliable 98.95% 43
loss 1% reliable 72.00% 135,837
loss 3% unreliable 96.73% 43
loss 3% reliable 73.30% 138,030
A reliable media channel loses 31% of a clean 12 Mbps offer, and its delivery
barely moves as loss rises, so loss is not the binding constraint — it cannot
carry the rate. An unreliable one holds everything on a clean link and then
degrades by about the loss injected. The reverse channel differs by three orders
of magnitude.
Muninn wanted the unreliable lane and had no way to say so. It signalled the
intent the only way the API allowed, by passing no reliable expiry, and its own
test has asserted Unreliable and failed since it was written. With this change
it passes.
Delivery now comes from the advertised profile, resolved by `channel_delivery`
alongside the `channel_reliable_expire_after_ms` that already worked this way.
That removes the send path's private opinion about reliability, so the profile a
peer was shown and the behaviour it gets cannot disagree. The channels whose
semantics are fixed by design — schema reliable and ordered, realtime unreliable
— are untouched and pinned by a test.
`media_delivery: None` means Reliable, so no existing consumer changes
behaviour. It is still source-breaking for anyone constructing the three options
types with a struct literal rather than `::client(...)`: two sites in
cultmesh-rs, four in Muninn, seven across the test suites here, all mechanical.
An earlier draft claimed Muninn was unaffected; the compiler disagreed.
The `examples/media_delivery_probe.rs` added here is what produced the table, so
the claim can be re-run rather than believed. Note its sender-side wire-cost
figure is unsound for the reliable case: the sender stops draining after five
seconds while retransmission is still in flight, so those byte counts
under-report. The delivery percentages are receiver-measured and stand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four of cultnet-rs's ten DatabaseEntry byte fields lack
#[cultcache(key = N, bytes)] and so serialize as MessagePack
array-of-integers rather than bin: IdunnDeploymentBrakeRecord.signature,
IdunnDaemonHealthTrustBindingRecord.signer_public_key,
GameCultServiceTrustAnchorRecord.signer_public_key, and
IdunnAuthenticatedProviderHealthProjectionRecord.signature. Converting
them touches live stores, so the question is what breaks.
Measured rather than reasoned, and the answer is the opposite of what I
expected. Reading is compatible in both directions: serde_bytes::ByteBuf
accepts an array marker, and a plain Vec<u8> accepts bin, so the flip is
neither a hard read break nor a one-way door. No dual-read path is
needed and reverting the binary is survivable.
What does change is reproduction. A record written before the flip can
be read but can never re-encode to the bytes it was stored as, and its
canonical digest moves with it. That is the cost, and it lands on
byte-exact store checks and canonical_sha256, not on readability -- so
the migration is a re-encode question, not a compatibility one.
Also pins the size difference the media work is chasing: 406 bytes as an
array against 278 as bin for a 256-byte payload, 1.46x, independently
matching the 1.45x measured on an 848-byte payload.
cargo test --test byte_slot_migration -> test result: ok. 4 passed; 0 failed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three media payload fields take the derive's `bytes` attribute, so they
serialize as MessagePack `bin` instead of an array of integers. An 848-byte
payload goes from 1234 bytes on the wire to 851 — 1.45x off every media byte,
on the one link measured to be starved.
This is a correction to the previous commit's reasoning, not a new capability.
That commit claimed the derive could not emit bin and left a test ignored
against a hint that needed designing. The hint has existed all along:
cultcache-rs-derive parses `bytes` at lib.rs:92, serializes through
serde_bytes::Bytes at :135, and decodes through ByteBuf at :154 and :170. What
is inert on a DatabaseEntry struct is `#[serde(with = "serde_bytes")]`, because
the derive reads only its own attribute namespace — which is a different and
much smaller claim than the one that was made.
The ignored test is now un-ignored and passing, and the tolerant assertion it
was propping up is exact again.
Four fields elsewhere are missing `bytes` and are deliberately untouched here:
IdunnDeploymentBrakeRecord.signature, IdunnDaemonHealthTrustBindingRecord
.signer_public_key, GameCultServiceTrustAnchorRecord.signer_public_key, and
IdunnAuthenticatedProviderHealthProjectionRecord.signature. Those carry stored
state and signing history — six trust anchors live in the deployment projection
on yggdrasil, and the brake store is byte-exact checked — so they need
sequencing with a re-sign rather than a flag flip. Owned by the Swarm Migration
work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four record types described a media stream and nothing about who produced it,
while living in odin-core under a `muninn.` prefix. Every field is
producer-agnostic; the name was the only coupling, and a second producer would
have had to impersonate Muninn or fork the shape. They move to cultnet-rs as
`gamecult.media_*`, matching the prefix already used for cross-service contracts
like gamecult.runtime_presence_health and gamecult.service_trust_anchor, while
cultmesh.* stays reserved for session primitives.
Field order and key indices are carried over untouched, including the `default`
on missing_video_chunk_keys. The tests pin what a consumer in another language
is entitled to rely on: positional layout, the slot each field occupies, and
that a record written before that field was appended still decodes.
One test is ignored rather than passing. Byte payloads currently serialize as
MessagePack arrays of integers because the derive writes its own Serialize and
ignores serde attributes; C#, TypeScript and Python all emit bin, which makes
Rust the divergent runtime and costs 1.45x on every media byte. Fixing it needs
an opt-in hint in cultcache-rs-derive rather than a global change, because
signatures are taken over the whole positional serialization and canonical
hashes over the same bytes — flipping byte encoding estate-wide would invalidate
the live Idunn authority chain. That work is sequenced by the Swarm Migration
session; the ignored test un-ignores when the hint lands.
Parked on its own branch. The Muninn and Odin ends of this move are not done:
odin-core still declares these types, and muninn-daemon still imports them from
there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dual-proved runtime presence contract existed only in cultnet-rs, so no
TypeScript service could make a statement Odin admits as presence. Odin files
anything else away as an ordinary document and never produces a topology
correlation for it, which is why Heimdall could warm forever without being
promoted.
The port is what it should be: the same fields at the same key slots. The Rust
derive serialises a positional tuple indexed by those slots, so the TypeScript
side names the slots and fills the same 24. Both proofs cover one payload --
the complete record with both signature fields empty -- and the two signing
messages use the domain separation their respective identities already define.
Conformance is against vectors emitted by cultnet-rs, not a fixture written
for both sides at once, which would agree with itself and prove nothing:
`cargo run --example runtime_presence_vector -p cultnet-rs` regenerates them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Odin carried defineOdinDocuments: 45 CultCache document-type definitions for
documents that cross service boundaries. Eve surfaces and provider
advertisements, Ghostlight, Heimdall, Idunn, Muninn, Sleipnir, Aetheria,
VoidBot, Stonks. Almost none of it is Odin. It lived there because Odin was
written first.
That was tolerable while Odin was the only consumer of consequence. It stops
being tolerable when Hermodr moves to its own repository, because the choice
then is a published contract or two definitions of one wire format, and the
second is the failure the Hermodr extraction exists to remove.
The catalog is now defineSwarmDocuments here, beside defineDocumentType, which
is what builds it. defineDocumentType stays injected rather than imported: that
is how it worked in Odin, it keeps the catalog clear of the generic signature in
./document, and it lets a caller supply another runtime.
Ported as a mechanical transform rather than retyped, then verified rather than
assumed: serialising all 45 definitions from both implementations against the
same probe gives byte-identical output and identical key sets. The 43 callbacks
that gained annotations are named DocumentValue rather than bare any, because
these definitions predate typing and read fields defensively, and pretending
otherwise would be inventing types nobody has verified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Odin carried a 324-line module named idunn-rudp.cjs. Thirteen of those lines
mentioned Idunn, and most were error strings. The rest was CultNet client work:
socket binding, RUDP handshake, Ed25519 signing, packet receipt, msgpack
framing. It sat in a consumer because that is where it was written, not because
that is where it belonged, and it was about to be duplicated when Hermodr moves
to its own repository.
The transport now lives here as signed-daemon-health, beside the RUDP session it
builds on. The receiving service supplies a contract naming its connection id,
signed schema, and unsigned diagnostic schema; nothing service-specific is
hardcoded.
The signature and identity domains are deliberately not parameterised. They are
the shared gamecult.provider-health domains, and a caller able to vary them
could silently produce statements no verifier accepts. The signing purpose is
the caller's signed schema id, so a statement signed for one service cannot
verify as another's.
The module also loses the sibling-path assumption it was carrying: it no longer
resolves CultLib by walking up three directories from wherever it happens to
live, because it now lives in CultLib.
Type-checks clean and builds. Verified faithful by Odin's existing signed-health
test, which asserts exact canonical statement fields, the observation timestamp,
and Ed25519 signature verification against the ported implementation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
odin-core only re-exported this contract from Odin's tree while the
sibling health/trust records already live in provider_health_contracts.rs.
Moving it here lets idunn-daemon depend on cultnet-rs directly instead of
odin-core, clearing the way to extract idunn-daemon into its own repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultLib's packages/cultmesh-rs and Odin's vendor/cultmesh-rs had diverged
independently -- 87 shared symbols, 6 only here, 9 only there. Neither was
simply ahead of the other, so this is a merge rather than a fast-forward.
Odin's copy is the base because it is the deployed code, running in production
via Idunn, and because it carries a deliberate semantic upgrade: connection_id
was replaced by a per-request message_id correlation with validation requiring
raw callers to supply one. That brings in the RUDP document server (647 lines)
and its tests (553), which the original migration into CultLib left behind.
Layered back in from CultLib's lineage: soa(), get_all_with_keys(),
create_rudp_document_message(), and the plural batch publisher. The batch
function was rewritten off the removed connection_id onto the connect helper.
Both publish paths survive deliberately. They are different jobs, not rival
implementations: the singular path polls one receipt for application-level
acknowledgement and can report Invalidated, while the batch path sends and
then flushes at the transport level. That is also why the census framed the
30s-versus-300ms flush_timeout as a value conflict and was wrong -- one field
name, two mechanisms. The distinction is now documented at the call site.
Surviving connection_id references are the RUDP protocol's own connection
epoch and are unrelated to the removed options field.
cargo test: 16 passed, 0 failed -- 10 lib, 6 document-server integration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite finished its work in about 60 seconds and then never exited. Four runs
were left alive for hours, holding 11 node and 2 dotnet peers, and a stranded
peer holding cultnet_interop_peer.exe is what made two unrelated cargo builds
fail with LNK1104. In CI this is a job that burns its whole timeout on green
tests.
Two real defects, found by asking the stuck process what it was waiting on
rather than by guessing. process.getActiveResourcesInfo reported four UDPWraps
and two PipeWraps still held after the last test.
The four UDP sockets: eight tests bind a server socket with bindUdpSocket and
close only the transport wrapper in their finally, never the socket itself. Four
leaked per run, matching the four Python lanes that fail on a machine without
msgpack -- the wrapper is built inside the try, so when the peer's readiness
rejects it never exists to be closed, and the raw socket is orphaned. Now closed
through a guarded helper that tolerates a transport having adopted it.
The teardown registration: t.after was registered after every `await ready`, so
a peer whose readiness rejected skipped the registration entirely and stranded
every peer already spawned. Moved ahead of the spawns at all four sites.
With those fixed the suite still does not drain on its own, so test:interop now
passes --test-force-exit. That is safe only because cleanup demonstrably
completes first: a forced run now leaves zero peers behind, where before it left
nine. Forcing the exit without the fixes above would have guaranteed orphans
rather than prevented them.
Verified: 65 seconds, exit 1, the expected 15 pass and 12 fail, no surviving
processes. The 12 failures are the Python lanes, which cannot run on this
machine and are unrelated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rust consumers pin a CultLib revision through cargo git dependencies, which is
how Ghostlight, Epiphany, CodexConnector and Odin already consume the core
stack. npm and pip cannot take a package from a subdirectory of a git repo at
all, which is why the TypeScript and Python consumers vendored forks instead:
there was nothing else to depend on. This gives them a versioned artifact.
Tagging cultcache-ts-v* or cultcache-py-v* builds, tests and publishes. The tag
version must match the manifest version or the workflow refuses rather than
shipping a mislabelled artifact. PyPI uses trusted publishing, so no long-lived
token is stored; npm needs an NPM_TOKEN secret.
cultcache-ts is now @gamecult/cultcache-ts. The scope is not cosmetic: it makes
the package unambiguously org-owned on a public registry, and it is what lets
CultLib hold the name rather than racing for an unscoped one. CultLib's own
cultmesh-ts and cultnet-ts and their imports move with it; all three packages
typecheck and cultcache-ts passes 12 of 13 tests, the exception being the
cross-runtime store test that shells out to Python, which cannot run on this
machine because its Python has no msgpack.
Licensing was inconsistent with the repo. cultcache-py declared itself "Private
GameCult infrastructure" and cultcache-rs and cultmesh-rs declared nothing,
inside a repo whose LICENSE is MIT and whose every other package says MIT. All
three now say MIT. That was load-bearing here: a package that calls itself
private is not one to push to a public registry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Surveying the swarm's vendored copies, the divergence is far smaller than the
number of forks suggests. Against CultLib, Epiphany's CultCache holds nothing
unique, CodexConnector's holds nothing, VoidBot's CultCache-TS holds nothing,
and Ghostlight's CultCache-Py holds nothing. What the forks did hold was mostly
reach: things CultLib has but does not expose.
raw_document_record_from_envelope was private here and public in every Rust
consumer fork. It is public now. A first pass called it missing, which was a
visibility difference read as an absence -- a `pub fn` scan cannot tell those
apart.
CultMesh gains get_all_with_keys and soa, thin delegations to CultCache, which
does support both. Those were genuinely absent rather than hidden.
cultmesh-rs also needed the media expiry field on its document transports; it
was not rebuilt when cultnet-rs grew that field because the earlier pass tested
only two of the three Rust packages.
cultcache-rs 53, cultnet-rs 107, cultmesh-rs 7.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first pass put the media deadline on the transport options and carried it as
a runtime field on the connection and hub. That is a second answer to what a
channel does, able to disagree with the profile the peer was actually shown.
CultNetTransportChannel now carries reliable_expire_after_ms and the send path
resolves the deadline from the profile, so there is one answer. The runtime
fields are deleted rather than left as a shadow copy. The transport option
remains as the knob that populates the profile, not as a parallel authority.
Odin's fork had the field in exactly this place, which is how the gap surfaced:
its muninn test reads it off the channel descriptor.
This is wire-visible, unlike the rest of the expiry work. The field is optional
and skipped when absent, so a profile that does not set it is byte-identical to
one from a runtime that has never heard of it -- but the RUDP profile does set
it on the media channel, so Rust peers now advertise it. Verified against the
interop lane: TypeScript, C# and Kotlin all accept the profile unchanged, in
five passing lanes each. The Python lanes cannot run here because this machine's
Python runtime has no msgpack, and they fail identically before and after this
change.
The other four runtimes should grow the field so a profile round-trips through
them without losing the channel's declared behaviour.
cultnet-rs: 107 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A first pass at Odin's vendored CultCache compared only its uncommitted diff
against CultLib and concluded nothing else was missing. That was wrong: a
whole-file symbol comparison found CultCacheExpectedEnvelope and a per-entry
compare_exchange that CultLib lacks and Odin's crates import. Comparing a diff
tells you what changed recently, not what the fork uniquely holds.
Unlike the snapshot form this constrains only the identities it names, so an
unrelated concurrent write does not lose the race. Expecting `None` makes it a
create-once claim, which is what an admitted-generation owner needs.
The expectation-identity guard is carried over as written even though it is
unreachable: a condition whose envelope names a different identity is already
rejected by the value comparison above it. Left in place rather than quietly
dropped, since it costs nothing and the lookup it depends on may change.
cultcache-rs: 53 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two capabilities existed only in Odin's vendored forks, which is why the forks
could not be deleted. Both are ported here with tests so Odin can consume
CultLib instead.
cultnet-rs gains reliable send expiry. A reliable packet may now carry a
deadline; past it the packet is dropped instead of retransmitted, and the count
is reported through session and transport stats. Live media tolerates loss but
not lateness -- a frame retransmitted after its moment has passed is worse than
a missing one -- so the media channel takes a 75ms default while state channels
never expire. Handshake packets never expire either: a dropped connect is not a
late frame.
The deadline is fixed when a send is admitted, so time spent waiting in the
backlog spends it exactly as time spent awaiting an acknowledgement does. This
lineage has a queued_reliable backlog that Odin's does not, and a queued packet
promoted after its moment had passed would be as useless as a retransmitted
one. Expiry runs before the window is measured, so an expiring stream is not
refused for a backlog it no longer owns.
cultcache-rs gains compare_exchange_snapshot and a nonblocking
try_compare_exchange_snapshot. CultLib had neither, despite carrying the larger
body -- the two lineages had diverged in both directions. Contention is returned
as data rather than waited on, so the caller keeps retry and scheduling
authority. This is the primitive an admitted-generation CAS needs.
No wire format changes. Expiry is a sender-side policy and compare-exchange is
local storage, so the other four runtimes stay in agreement without matching
these refusals. They are still worth carrying eventually, and the parity ledger
already tracks that this kind of gap does not announce itself.
cultnet-rs: 106 tests. cultcache-rs: 51 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found while checking what would be lost by deleting Ghostlight's vendored fork.
Its copy of transport.rs carried a guard CultLib never had: tcp_framed receive
compared the declared payload length against the max_payload_bytes the profile
advertises for the schema channel, and refused before allocating.
Without it the length in a four-byte header is taken on trust and used directly
to size a buffer, so one header reserves whatever the peer names before a single
payload byte arrives. The guard sits ahead of the allocation, which is the only
place it does any good.
It is inert unless a profile advertises a maximum, so the test advertises one.
This is the last thing unique to that fork. Everything else in it is now here or
was already here, which is what makes the fork safe to delete.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clears the cultnet half of the retained debt. The two unported commits from the
retiring GameCult/cultnet-rs lineage were not portable as patches: this lineage
had reworked the same receive path with sequence domains and ACK horizons, and
applying them raised nine conflicts that would have reverted that work. Most of
what they added already existed here by another route -- received-sequence
history, fragment-set count and reassembly bytes are all bounded already. So
the gaps were taken rather than the patches.
Three were real. The per-channel maps grew without limit, and a peer picks its
own channel ids, so ordered and sequenced channel tracking is now capped at 64
each. The out-of-order hold buffer grew without limit, and a peer picks its own
send order, so it is capped at 1024 frames and 4 MB; sizes are computed from the
buffer rather than tracked in counters, which cannot drift.
The fourth was the sharpest. remember_received ran before the duplicate
early-return, so a single forged packet far past the current highest advanced
the receive state beyond every sequence still in flight and starved the
session. Such a packet is now refused without being remembered.
Reliable sequence exhaustion aborted the process through expect(). The capacity
gate that already guards the send window now also refuses when the sequence
space cannot cover the request, and allocation saturates rather than panicking
so no future path can reintroduce the abort.
Five tests cover the bounds and both refusals. cultnet-rs passes 103,
cultcache-rs 48, cultmesh-rs 7, and the TypeScript-Rust RUDP interop lanes pass
5 of 5 with the hardened peer, so legitimate traffic is unchanged.
The parity ledger now records what this actually leaves owed: these are
refusals, not wire-format changes, so the interop lanes stay green whether or
not the other four runtimes match. The gap will not announce itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Taking the standalone body as the CultCache base dropped a0b31b8, and it was a
real regression rather than a missing convenience. A record whose catalog entry
is absent -- written by another runtime, or older than the catalog it was
stamped against -- made decode_store_snapshot fail, and one such record failed
the entire pull. Recovering the type from the payload is what "across runtimes"
meant in that commit's title.
Restored in full: the payload is a MessagePack array whose first field is the
schema version, so the schema name is recoverable from the record itself; the
registry now also indexes schema name to entry type and refuses a name claimed
by a second type; and pull resolves a persisted type that is either a
registered type or a recovered schema name. The stale schema id is preserved on
the envelope rather than rewritten.
743eaa5's durability half needed no code. The standalone lineage reached the
same place independently through crash-atomic single-file snapshots and
abandoned staging cleanup, and its cleanup carries the same protections: only a
candidate named for this exact store plus a canonical UUID is removed, and
anything that is not a regular file is skipped. Untested, though, which is how
a careful deletion rule quietly becomes a careless one. Now asserted: an
abandoned canonical candidate is removed while a non-UUID lookalike, a bare
.tmp suffix, and another store's candidate all survive, and the committed
snapshot still reloads.
48 tests pass. This clears the cultcache half of the retained debt from b672fb7.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Separating the reliable and lossy sequence domains landed in Rust, TypeScript
and C# on a branch. The Python and Kotlin halves were written at the same time
and left uncommitted in the worktree, so three runtimes spoke the new contract
and two spoke the old one -- the exact cross-runtime disagreement this
repository exists to prevent.
Both were already complete and implement the same three rules as the others:
ordered delivery requires reliability, sequenced channels carry their own
per-channel counters with latest-wins delivery, and duplicate suppression is
gated on packet.reliable so lossy datagrams are remembered nowhere.
Python also gains the three tests Rust, TypeScript and C# each received and it
did not: lossy packets cannot create reliable ordered gaps, unreliable
sequenced delivery is scoped to its channel, and unreliable ordered delivery is
rejected. Python passes 141.
Kotlin is committed unverified. This machine has no JVM or Gradle, and the
package carries no runtime-local test file, so its only gate is the interop
lane in CI. The change mirrors the four runtimes proven here line for line, but
mirroring is not evidence and CI is the witness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Separating the sequence domains gated duplicate suppression on packet.reliable
in Rust, TypeScript and C# alike, so lossy datagrams are remembered nowhere and
a replayed one is delivered again. That is the intended contract -- lossy
traffic is fire-and-forget and its history is bounded by not existing -- but
replay_history_is_bounded_and_idle_hub_sessions_expire still asserted the old
behaviour with a lossy packet and had been left failing on the branch.
The three tests the change added in each runtime cover ordered-requires-
reliable, per-channel sequenced scope, and lossy gaps. None covers the bounded
replay window, which was this test's real subject, so removing the assertion
would have dropped the only coverage of a security property.
The lossy assertion now states the new contract, and the reliable domain gets
its own replay check: 4,100 acknowledged reliable sends, then the oldest
replayed and rejected by the window.
Rust 151, C# 148, TypeScript 48, Python 138 all pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude Code reads CLAUDE.md, not AGENTS.md, so this repo's doctrine was
invisible to it. This one-line import makes the existing AGENTS.md load
without duplicating or renaming it. Codex is unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
exact_reliable_acks_clear_more_than_one_ack_window_of_fragments has been
failing on main. It was not a defect: send_many returned 32 packets where the
test expected 40, and 32 is exactly CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS.
The reliable send window landed and this test kept asserting the pre-window
behaviour, so a test whose name is about crossing an ack window had stopped
crossing one. It now asserts that a window's worth leaves immediately, that
the remaining eight fragments queue, and that acknowledgements promote them
until every fragment is delivered -- the same loop packages/cultnet-rs/tests
/cultnet.rs already uses for this contract.
All three Rust crates are green: 46, 95 and 7.
The NuGet packages rule hides every language package until one is named in
the re-include list, and cultmath, cultmesh-browser and cultmesh-kotlin were
never added. Nothing is currently lost -- every source file in them is
tracked -- but any new file added to those three would have been dropped in
silence. cultmesh-rs hit this on the way in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Exact acknowledgement receipts landed; bounded session memory and hostile
sequence rollover rejection did not. Until they do, this runtime accepts
sequence and reassembly input the retiring lineage rejects, so its repository
cannot be deleted yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ported from GameCult/cultnet-rs 9dcb88d into CultLib, where the wire contract
belongs. Ghostlight's committed idunn_health.rs imports
CultNetRudpReliableSendStatus to hold an Idunn process write lease current,
and the symbol existed only in the standalone lineage, so nothing could
consume this contract from its canonical home.
Three reconciliations were needed because both lineages reworked RUDP
independently since the 014f998 copy-in:
- reset_peer_state: kept this lineage's version, which also resets the
sequence and fragment counters and clears queued_reliable, and folded in
only the imported change that matters -- rotating session_scope so a peer
reset invalidates outstanding receipts. The imported body also cleared
highest_delivered_sequenced_by_channel, a field the sequence-domain work
renamed away here.
- channel_send_options is a free function in this lineage, not a method.
- The imported guard rejected reliable channels with an expiry. This lineage
has no reliable-expiry concept at all -- the only mentions of expiry in the
file arrived with this patch -- so every reliable channel is already
non-expiring and the guard reduces to the reliability check. A comment
marks where to restore the expiry half if expiry is ever added.
94 tests pass. The one rudp_server_hub failure is pre-existing and reproduces
on clean origin/main.
The other two standalone RUDP commits are deliberately not ported. Bounding
session memory and rejecting hostile sequence rollover rebuild the receive
path -- stale-sequence rejection, per-channel tracking caps, reassembly and
ordered-buffer byte accounting -- in the same code this lineage reworked with
sequence domains and ACK horizons. Merging them changes wire-observable drop
and reassembly behavior, which needs the cross-runtime interop harness to
validate, not a local build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The only Rust CultMesh implementation in existence was 1,363 lines inside
Ghostlight's vendor directory. It was in no library repo and no standalone
repo, so it had never been checked against the browser, Kotlin, and
TypeScript CultMesh bodies it must interoperate with.
It is not a duplicate of the facade in cultnet-rs. That one is the small
surface the parity table claims for Rust: peer cards, authority leases,
endpoint parsing. This is the stream and session layer -- node, document
sets, RUDP document publication, stream descriptors, negotiation, stream
catalog, and shared-memory frame rings -- which the same table lists under
Rust's "Not claimed". A consumer grew the unclaimed surface because there was
nowhere else to put it.
Its path dependencies already matched this layout and it builds against
CultLib's cultnet-rs unchanged, so it does not depend on the forked RUDP
extras. Seven runtime-local tests pass, and CI now runs them.
The parity table gains no claim from this. The Rust row says plainly that no
interop lane exercises the stream surface, and expansion work records that
runtime-local tests are not a parity claim. Landing code is not evidence.
packages/cultmesh-rs needed an explicit .gitignore re-include; the NuGet
packages rule hides every language package until one is added by name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The standalone GameCult/cultnet-rs holds six modules CultLib never received:
runtime_authority_contracts, service_identity, provider_health_contracts,
idunn_deployment_brake, idunn_lifecycle_brake, and snapshot_query. They own
Idunn Expected incarnations, dual-proved runtime activation, process write
leases, service-signed presence health, and Odin topology correlation -- the
wire contracts every GameCult daemon must agree on.
They belong here because wire parity conformance lives here and nothing
outside it is checked for cross-runtime agreement. Held as source to be
copied, this contract already forked: Ghostlight vendors it at e542225 while
its own authority map names f4ab237, and the two disagree on whether Present
may coexist with a disagreement. One copy makes them exclusive; the other
separates presence from readiness and gates through into_undisputed_present.
A single owner under the parity harness is the fix; re-vendoring is not.
None of the six touch RUDP, so they land without disturbing the protocol
core. rudp.rs remains genuinely forked -- 140 hunks between the lineages,
with reliable windows and ACK horizons on this side and bounded session
memory, exact acknowledgement receipts, and hostile rollover rejection on the
other. That reconciliation needs the cross-runtime interop harness and is not
attempted here.
Adds ed25519-dalek, zeroize, and the Windows Foundation/Cryptography features
the credential paths require. 96 tests pass. The two rudp_server_hub failures
are pre-existing on this base and reproduce at 230ace6 untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
packages/cultcache-rs forked from GameCult/cultcache-rs at the 5c69bfc
monorepo import and the two lineages then diverged in both directions. The
standalone repo took 28 commits of engine work CultLib never saw: typed SoA
projection, atomic heterogeneous batch publication, single-entry and
conditional batch CAS, a transactional keyed redb backend with serialized
cross-process opens, lockless provider snapshot reads, RAII read-only shared
snapshots, snapshot-fenced replace/append, crash-atomic single-file snapshots,
compaction fences, portable and canonical byte fields, and unified retained
storage authority. That body is 3,714 lines with 48 tests against CultLib's
1,554 with 18.
CultLib is the canonical home because wire parity conformance lives here and
nothing outside it is tested for cross-runtime agreement. A runtime-specific
core-stack repo cannot hold the engine every other runtime must agree with.
The standalone body lands as the base rather than merging 28 commits backward.
It is already proven against a CultLib-derived cultnet-rs: Ghostlight vendors
exactly this pairing and compiles. Here, cultcache-rs passes 46 tests and the
cultnet-rs RUDP gate passes 20 with no source change.
Retained debt, not carried forward: a0b31b8 recovered schema-stamped records
missing catalog entries (SchemaStamped, infer_schema_name,
infer_schema_version_from_payload) and 743eaa5 hardened snapshot replacement
(SnapshotReplacementStage, CommittedSnapshotDurabilityUncertain, stale
candidate cleanup, with_exclusive_lock_using_unlock). Their tests were
white-box against internals this body does not have. The standalone
independently made single-file snapshots crash atomic and cleaned abandoned
staging files, so the durability guarantee is probably held under other names
-- probably is not proven. Re-establish both as black-box tests against the
new API before the standalone repo is retired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>