Cut 5 (TypeScript). The cache no longer mirrors: StoreRoute and its mirrors
list are gone, #resolveRoute returns the single home store (the store that
claims the type, else the generic store), and put, putEnvelope and delete write
exactly one store.
addBackingStore now refuses a second generic store and a type already claimed
by another store, attaching nothing when it refuses, matching C#. No bytes
change; nothing in CultLib attached more than one store.
Tests: rejects a second generic backing store, rejects a type registered to
two stores, routes each type to its home store. The writer-by-reader interop
test now runs the C# write-routed mode and checks that catalog.cc and run.cc
are each a complete v1 snapshot holding only their own record, and that every
reader (TS, Rust, C#, Python) reads the routed catalog store.
Version 0.13.5 -> 0.14.0; cultnet-ts and cultmesh-ts depend on ^0.14.0 so the
workspace link still satisfies their range.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The C# reference publishes authorityRoutes with certificate: null for
loopback routes, and its own verifier treats null as "no certificate" in
local development. The TypeScript JSON schema only allowed the object,
so the browser client rejected every catalog the getting-started
provider served: "certificate: must be object", Odin could not resolve
the Verse, and the real browser/C# checkpoint has failed since f622ee1
(2026-08-18), behind the sample failure that hid it. TypeScript
consumers already guard route.certificate; the schema now accepts null.
The package-closure smoke also still spelled the browser identity with
providerId; it is authorityRuntimeId since c194bd4.
npm test --workspace packages/cultnet-ts -> 54 pass
npm run test:pack:ts -> passed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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>
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 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>
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>