The Unity list no longer calls MarkFlushSucceeded removed and places HeaderFor and the CacheBackingStore members where apicompat finds them. cultnet-rs gains a CHANGELOG. The store-composition contract moves DecodeDocument to the codec and states that a call's registry serves that call only. Docs only.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Identity: C2a's StoredSchemaId/StoredVersion owns; main's PersistedSchemaId, PersistedIdentity and both MarkWritten are deleted. Stamp rule: ReadStore judges the stamp before the body. Flush over damage refuses (merge-r1-precedence). Mesh single-file writes route through SingleFileMessagePackBackingStore. TS: store-file.ts commits, C2a's header handling kept. Rust: C2a's readable_store_format in front of the grown decoder. Build not yet verified at this commit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raw v1 is decoded by the enum's derive alone (SnapshotResponseRawV1Rows,
the v1 arm and its helpers deleted); the five raw families carry
skip_deserializing so the hand reader is their only decoder. The four hand
readers refuse a duplicated key; refusals carry a schema-shaped
schemaVersion context and never a received value. README describes only
the raw lane. A same-length str-payload credential forgery is pinned.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mutation survivors of the first run: validate_message -> Ok(()) and the
Some(18) arm of decode_canonical_signed_payload.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deletes CultNetDocumentRecord, the DocumentPut and SnapshotResponse variants,
parse_cultnet_message, require_typed, round_trip_typed, the eight replication
functions and the two catalog schemas (parked at tag
parked/cultnet-rs-dynamic-document-lane). decode_cultnet_message_from_slice is
the one decode; raw v1 rows, identity credentials and the signed-record readers
decode through cultcache_rs::decode_payload. Adds tests/decode_door.rs. 0.2.0.
Not yet compiled at this commit (verification runs on Yggdrasil).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sixteen conflicted files resolved by the spec's rule: main owns payload
ownership, bounds and the Clock seam; C2a owns persistence semantics and exact
binding. MintStoredAt is internal static (previous, now); the cache and the
store pass their cache's Clock. Package versions are main's. The cultmesh-ts
payload-parse acceptance and its test are deleted under retire-schema-alias;
peer snapshot reads take the schema's own id, else an id the definition lists
as compatible. The held firstRecordAtKey fallback is removed: main's handles
take definitions only, so nothing could reach it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lifecycle brake, signed daemon health and runtime authority records check
an exact positional array length before decoding, so they have no undeclared
slot to skip; they return to the plain decoder. The media records have no
length check and their derive skips undeclared trailing slots, so all five
are held by the test.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Document payloads decoded with rmp_serde::from_slice in provider_session,
media_stream_wire, idunn_lifecycle_brake, provider_health_contracts,
runtime_authority_contracts and the CultNet message decode in contracts.rs now
decode through the one depth decision. The duplicated schema-v0 branch in
decode_cultnet_message_from_slice is deleted.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
replication's document_record_from_envelope and the service identity credential read no longer
call rmp_serde::from_slice on a stored payload, so the 256 bound holds there too.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
R1's rules win where both sides decided the same question: a zero-byte file is
not a store (refused as unreadable in every runtime), only nothing at the path is
empty, a dangling link is an I/O error, refusals never echo stored values and echo
a header only as cultcache.store.v<digits>, writes report Rejected, NotReplaced
and ReplacedNotDurable. C2a's rules win on what it added: the v3 element-id
header, variants, whole-file proof, typed unreadable errors in C#, TypeScript and
Python. Rust keeps R1's CultCacheStoreUnreadable{path, kind}; C2a's
StoreUnreadableError is deleted. The readability manifest's zero-byte row is
refuses x4.
Not yet built or tested: verification follows in the next commits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
end_unsendable_session put the io::Error's Display on the wire: the
platform's wording, which differs by OS and by which packet failed.
cultnet-rs gains send_error_code, the fixed name of a failed send's
cause (EMSGSIZE, EINVAL, EAFNOSUPPORT, or UNKNOWN), matching the codes
the TypeScript goodbye already carries; is_permanent_send_error is now
"has a known code". The goodbye reads "packet could not be sent: CODE".
ResponseSendFailed carried an ErrorKind, which lost the cause (EMSGSIZE
has no kind and read as Uncategorized). It now carries the same fixed
code, and the session ends where the send failed, over the real error,
so the goodbye and the caller's rejection name the same failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A peer's undecodable or unhandleable message ends only that peer's session and never
escapes Poll (the C# RUDP schema server's live crash). A send failure's goodbye belongs to
the generation that sent it. TS transient send failures are counted, not reported. The
interop harness finds the Rust peer through cargo metadata.
Soul passes 1-4; batch 5 closes pass 4's findings. Merged tree a892e117 verified on
Yggdrasil: GameCult.Networking 481, Mesh 264, cultnet-ts 198, interop 27/27 with
CARGO_TARGET_DIR set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." CultNetDocumentRegistry::register inserted into its
type and schema-id maps unconditionally, so the last binding for a schema id
won, as the TypeScript registry did before 9adea366.
- register returns Result and refuses a binding whose type already holds
another schema id, or whose schema id another type holds, with
cultcache_rs::SchemaConflictError: the new binding's schema id, both
document types (a binding carries no schema name), an empty record key.
Nothing is bound on refusal. Binding a type to the schema id it holds
again replaces the binding.
- Call sites propagate the refusal: the cultmesh-rs registry macro, the
interop peer example, and the tests.
- Tests renamed to what they test: raw snapshot replication hydrates the
receiver's own type for the schema; a reactive document coalesces direct
member writes.
- New test: both claimant shapes in both orders, the first binding intact
afterwards, and a same-type rebind replacing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator: "FEC: Please own the math." rs-gf256-v1 implemented in
media_fec.rs, wire-identical to the crate (1,795 geometries, independent
derivation, 6,520 cross-version recoveries, 60k hostile blocks identical).
lru and instant advisories gone; 13 fewer dependencies. combine uses one
product row per coefficient: encode ~1.0-1.4x, decode ~1.2-1.4x the crate.
Follow-up: k^3 inversion makes 255+1 recovery ~124 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Import lists in cultnet-ts index.ts and operation-service.ts and the
cultnet-py test block conflicted; both sides are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The consolidation guard accepts cultnet-rs vendored schema copies (the lane
was red for 40 main runs). Every CI step now fails on any failed command
(job default shell bash -eo pipefail); proven red on a throwaway branch.
The cultnet-py explicit-bind test stays on loopback; the Rust drift guard
lists the vendored directory. CI run 36723314611 green at 59d85557.
Follow-up: CI never runs schema_discovery_contracts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The equality test compared only the 22 names in a hand-kept list, so a schema
copied into packages/cultnet-rs/contracts/cultnet/ without joining the list
escaped it. Every *.schema.json under the vendored tree is now found by listing
it and compared against its canonical copy. The presence test is gone: include_str!
already fails the build for an embedded schema that is not vendored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Document server: a reset reported by a receive is idle, not an error
(Windows only, where the system reports it; run on Starfire).
- Document server: no ack follows a reply that can never be sent. The
guard is not decoration: without it the ack's success overwrote the
failure and the session that owed the Pong was kept.
- Hub: a send refused at any fragment index delivers nothing of it; at
index 0 the next send arrives whole (Soul's probe, committed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul measured the owned math at 1.7-2.6x the crate's time at realistic sizes
(16+4 x 1200 B encode 64k -> 122k ns), because combine called gf_mul per byte:
two zero checks and three lookups. combine now tabulates each coefficient's
256 products once and spends one lookup and one XOR per byte; coefficient 0
is skipped and 1 is a plain XOR. The construction and every wire byte are
unchanged: the known-answer fixture passes untouched.
tests/media_fec_bench.rs is an ignored, dependency-free timing test over the
public surface (4+2 at 160 and 1300 B, 16+4 at 1200 B, 255+1 at 1300 B):
cargo test --release --test media_fec_bench -- --ignored --nocapture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server accepting a fresh Connect can send before its Accept is
acknowledged, so no ack of the new generation prunes the acknowledged
sizes; only `end_session` clearing them keeps the ended generation's bytes
from counting against the new window. Rust, TypeScript, Python and C# each
pin it: 4 MiB acknowledged above a lost packet, a fresh Connect, and the
server's first 1 MiB send of the new generation goes out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`send` checked `window_admits` before `send_many` purged deadline-passed
packets, so the first direct send after the lowest unacked packet expired
was refused for acknowledged bytes that `send_many` would already have
released. `send` now purges first, as `send_many` does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CultMeshRudpSocketOptions defaulted bind_host to 127.0.0.1 for servers and
clients alike. On Windows a socket bound to loopback cannot send off the host
(os error 10051), so a default client could never reach a remote endpoint such
as rudp://10.77.0.1:17872.
bind_host is now Option<String>. Unset, a server still binds 127.0.0.1; a client
binds loopback for a loopback endpoint and the unspecified address of the
endpoint's family (0.0.0.0 or ::) otherwise. An explicit bind_host wins for both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust hub and C# listener: 1,100 one-byte fragments, the first can never be
sent, and nothing of the send stays queued.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Operator ruling, 2026-09-30: "FEC: Please own the math."
reed-solomon-erasure 6.0.0 pulled lru 0.7.8 (RUSTSEC-2026-0253, unsound) and
instant (RUSTSEC-2024-0384, unmaintained) into every cultnet-rs consumer.
media_fec.rs now implements the construction its docs already state, read
against the crate's source rather than its names: GF(2^8) reduced by 0x11D
with 2 as generator, the n x k Vandermonde matrix over the evaluation points
0..n-1 with 0^0 = 1, E = V * inverse(top k rows), parity = rows k..k+m of E,
and recovery through the inverse of E's rows for the first k present shards,
which is the row choice the crate made.
About 130 lines: const log/exp tables, a Gauss-Jordan inverse, the encoding
rows, and one combine loop. It allocates only the block's shards and three
k x k matrices, so the decoder's bound is still set by the wire validators
(<= 256 shards of <= 64 KiB). The shard functions no longer return errors
that only the crate's own checks could raise; every caller already bounds k,
m and the shard length.
The generator test is deleted with the crate; the fixture keeps its output and
names the commit that ran it. New: a seeded property test over 300 random
(k <= 48, m <= 24) geometries, any e <= m erasures recover exactly and m + 1
are refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/fixtures/media_fec_rs_gf256_v1.kat holds what reed-solomon-erasure 6.0.0
produced at 73bf4421: the parity bytes of every standard video geometry
(k = 1..=16), standard audio 4+2 at 1, 37 and 1300 bytes, a 4000-byte shard,
all-zero and all-0xff data, and the policy's edges (1+1, 2+1, 255+1, 1+255,
2+254, 128+128, 200+56, 250+6), plus 120 erasure patterns mixing data and
parity. Its header defines the data, so another runtime can meet it.
Two tests drive the codec's public audio path, which takes any (k, m): every
case's parity matches byte for byte, and every pattern recovers the erased data
from the fixture's own parity bytes. Both pass against the crate here, before
it is replaced.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The injected permanent failure now takes precedence over the injected transient
one, so a test can end a session on a peer that is also losing datagrams and
count exactly which datagrams were attempted afterwards. New tests: a hub
disconnect that can never be sent returns the error; a peer or session ended
in the resend loop is sent nothing more (Rust hub, document server, C#); the
goodbye reaches the peer; the Python server ends the session on a permanent
failure in a resend, an ack and an accept. The oversized-send test compares the
outstanding count with the one the hub's own unacknowledged Accept already
holds.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Before reed-solomon-erasure is replaced by an owned GF(2^8) construction, it
produces the vectors the owned code must meet. The ignored test encodes every
standard video geometry (k = 1..=16), standard audio 4+2 at 1, 37 and 1300
bytes, a 4000-byte shard, zero and 0xff data, and the policy's edges (1+1,
255+1, 1+255, 2+254, 128+128, 200+56, 250+6), and reconstructs four erasure
patterns per case, asserting the crate returns the original data.
It is run once and deleted with the crate; git keeps its provenance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hub and the document server swallowed every send failure as a counted
loss, so a 70,000-byte reliable frame returned Ok and was resent forever.
Failures now split in two. Permanent means the datagram can never be sent as
built (EMSGSIZE, EINVAL, EAFNOSUPPORT and their Winsock equivalents,
is_permanent_send_error in cultnet-rs); everything else stays a counted loss.
A caller-directed hub send returns the error and queues nothing: the packets
it created are withdrawn and their sequences reissued, so the peer never sees
a gap (a permanent failure after an earlier fragment left ends the session).
Inside a poll the failure ends that peer's session with a goodbye whose reason
names the error, reported to the hub caller as Disconnected, and never fails
the poll. The document server ends the session the same way and reports a
snapshot response it can never send as an application rejection. Tests pin
each path, the classification per OS, BytesSent after a healthy send and a zero
failure count for healthy sends.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The bytes counted above a lost packet stop counting once it is acknowledged;
every runtime now asserts that another MiB is admitted after the gap closes.
Rust prunes on expiry only when something expired, so the acknowledgement path
owns the pruning it can observe.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The bound summed only packets still awaiting an ack. Once frames above a lost
packet were acknowledged they stopped counting, so a receiver could be made to
hold more than 4 MiB behind one loss, and its hold buffer ended the session.
The sender now keeps the payload size of each acknowledged sequence above the
lowest pending one until the lowest advances past it, in Rust, TypeScript,
Python and C#. Tests per runtime: one lost packet, four acknowledged 1 MiB
frames, the fifth refused until the gap closes; and the floor moving to a new
lowest sequence.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cut 1's the_hold_buffer_takes_exactly_1024_frames test held 1,024 frames behind a
sequence the receiver never sees, by sending them all above one unacked packet.
Cut 3's flow window stops a sender 1,023 sequences above its lowest unacked packet,
so the test's sender was refused at frame 1,023. The missing packet is now
acknowledged by hand (the receiver never saw it), so the window slides and what
the test probes is the receiver's hold buffer, not the sender.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An Accept now counts only while the session's own Connect awaits it, and reliable
data is refused until the handshake seeds the watermark, so the tests' bare Accept
no longer connected the session. The helpers now pair a sender and a receiver
through CreateConnect/AcceptConnect in all four runtimes; the sender's Connect
is acknowledged, so nothing is pending on it when the tests begin. Rust also
takes initial_sequence as an Option.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cut 1: one watermark owns ordered delivery, seeded only by the handshake.
Cut 1b: every ending drops what the session still owed; flush is bound
to its generation. Cut 1c: a Connect repeats only by its sequence;
stale data and stale Connects are handled as TCP does (challenge Ack);
an unanswered attempt retries at +window-1; the session draws its
initial sequence. Wire bytes unchanged in all four runtimes. Five Soul
passes. Public option types changed: initial_sequence is optional.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a Connect awaits its Accept, only an Accept the client honours retires it. An Ack that names the Connect (a server's reply to a repeat or a stale copy) no longer wedges the client with nothing to resend. The Connect payload lives on the session, so a fresh attempt can always be built. The fresh attempt uses the abandoned sequence plus the receive window less one (serial), so a late copy of the abandoned Connect stays inside the new one's stale window; only a session's first Connect draws at random.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A frame at or before the seeded watermark is a duplicate of an earlier generation and is acknowledged, never delivered again (serial compare within the receive window). A Connect that precedes the current generation's within the window is a stale copy of an earlier attempt: it is answered like a repeat (Accept owed or Ack), changes no state and does not refresh liveness. A Connect unanswered for 3 s is replaced by a fresh Connect with a newly drawn sequence and the same payload. In server mode a Connect from another endpoint is always a new generation. The initial sequence is unset by default and each session draws its own; the public random helper is removed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
rs-gf256-v1 (construction documented, independent implementation matches
every KAT); audio parity with checked block arithmetic; every encoded
record must fit the producer's datagram budget (default 1431), and no
media record exceeds 64 KiB on encode, decode, send or reassembly. The
"audio" channel is retired. Muninn and Ratatoskr bump in lockstep at
Cuts 4-5. Three Soul passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mutation run showed the unfragmented receive path of the RUDP session was
undefended (only fragmented reassembly was tested).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The sized parity helpers collided with the existing fixtures of the same name,
so they are now sized_audio_parity and sized_video_parity. Two existing cases
encoded the old base-pts deadline rule: the corruption table expects the new
last pts_ticks message, the edge-of-bound cases admit a deadline at the last
pts, and the 254-shard audio case sets a deadline that covers its last slot.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>