Commit graph CultLib/packages
Author SHA1 Message Date
Meta Vens
e9d5ae2cad cultnet-ts: pin that a fragmented send over one datagram's size is sent
The EMSGSIZE pre-check measures a send's largest datagram, which is one
fragment when maxFragmentBytes is set. Nothing pinned that: a check that
compared the whole payload (Soul's S-TM2) would refuse every large
fragmented send, and large sends are live. A 70,000-byte send with
1,024-byte fragments must not throw and must arrive whole. The test
kills the mutant whose size check ignores maxFragmentBytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:23:47 +02:00
Meta Vens
d54110fe24 cultnet-ts: the send-failure tests send requests without response-only fields
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:14:20 +02:00
Meta Vens
31b6c09d64 cultnet-ts: the send-failure tests build operation requests the contract accepts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:08:51 +02:00
Meta Vens
73fbb264d6 cultnet-ts: a caller's send larger than a UDP datagram throws EMSGSIZE and takes nothing
Node reports an oversized datagram only to the send's callback, so the
caller's send used to succeed and the session ended later. The transport now
checks the largest datagram of the send against the socket family's UDP
limit (65,507 bytes for IPv4, 65,527 for IPv6) before the session builds
anything: the send throws, nothing is queued, and no sequence is taken, as
the Rust and C# listeners withdraw it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:28 +02:00
Meta Vens
2d76b8d1b6 Pin the reset before a refusal's goodbye (cultmesh-ts, cultmesh-py) and C#'s once-guard
The document servers' refusal tests sent an unreliable fragment, whose
sequence the session never records, so dropping the reset before the goodbye
survived. They now refuse a reliable fragment and check the goodbye does not
acknowledge it.

C#: a BeforeSend seam orders a caller's failing send between the polling
thread's ack and its send, so both threads end the same session; the peer
hears one goodbye.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:27 +02:00
Meta Vens
de8e2e1b88 cultnet-ts: an operation server's send failure belongs to the generation that sent it
A permanent failure reported after that generation ended (the client
connected again from the same endpoint) ended the generation that replaced
it and said goodbye to the new client. The callback now acts only while the
generation that sent the datagram is live, which is the one in `sessions`.

That makes two guards unreachable, so they are deleted: `peer.unsendable`
before the goodbye (the first permanent failure ends the generation, so a
second callback from it is already ignored) and the conditional delete (a
live generation is the entry at its key). A refusal's goodbye is sent
best-effort, as the transport and the document server send theirs, instead
of through the failure handling meant for datagrams a session owes.

Tests: late failure after a reconnect, for the transport's generation check
and for the operation server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:27 +02:00
Meta Vens
a288bb037a cultmesh-py: a peer's message that handling cannot serve ends that peer's session, never the RUDP thread
The RUDP loop caught only ValueError, so a subscribe frame with
`schemaIds: 5` raised TypeError in _handle_connection_message and ended the
thread for every peer. Any exception from handling one peer's messages now
ends that peer's session with a goodbye, as a refused packet does, and is
logged by its type alone: the error's message can quote what the peer sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:56:17 +02:00
Meta Vens
25ea551bd5 cultnet-ts: an operation handler's result belongs to the generation its request arrived on
When that generation ends while the handler runs (a goodbye, a refusal, a
timeout, a new Connect from the same endpoint), the response is not sent
and a failure sends no second goodbye: a client that connected again owns
the endpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:54:33 +02:00
Meta Vens
1f25195afe cultnet-ts, cultmesh-ts: a send the network refuses is a counted lost datagram, never an "error"
Node reports a failed send only to the send's callback, never on the socket's
"error" event. Emitting it as "error" from the transport ended any process
with no listener (invokeCultNetOperation among them) once per resend tick.
The transport and the document server now count it in sendFailures, as the
operation server, Rust and Python already do. The tests send to
255.255.255.255 with no error listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:51:47 +02:00
Meta Vens
2e94dc3aa3 Merge origin/main into hands/ack-cutD: Cut 3 flow window, RUDP client bind
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>
2026-09-30 15:49:04 +02:00
Meta Vens
b96420b606 Ack Cut D (Rust): pin the receive reset, the ack after an unsendable reply, and a refusal at every fragment
- 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>
2026-09-30 15:03:16 +02:00
Meta Vens
06f8a31cb5 Ack Cut D (Python): a packet one peer's session refuses ends that session, never the RUDP thread
session.receive raises ValueError on bad fragment metadata, and nothing
in _rudp_loop caught it, so one admitted peer could end the thread for
every peer. The server now ends that peer's session with "session refused
a packet", as the socket transport and the Rust servers do.

Tests pin that, and two D2 rules that were unpinned: a resend loop sends
nothing more to a peer it just ended (P1), and responses after one that
can never be sent are not sent (P3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:03:11 +02:00
Meta Vens
cc750d4e4c Ack Cut D D2 (cultmesh-ts): the document server's sends are guarded, classified, and a refused packet ends only its peer
Every socket send in the RUDP document server goes through
sendRudpDatagram. A Connect from UDP port 0 used to store a session whose
pending Accept threw from the resend interval on every tick, an
uncaughtException that ends the process. Now:

- a Connect whose Accept can never be sent starts no session and leaves a
  session already at that endpoint as it was;
- a datagram that can never be sent as built ends only that peer's session
  (goodbye, onSessionClosed); any other failure is a lost datagram,
  reported through onError, and ends nothing;
- a packet the session refuses (bad fragment metadata) ends that peer's
  session with "session refused a packet" instead of leaving a session
  that has recorded a sequence it never delivered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:03:05 +02:00
Meta Vens
a44e0199ac Ack Cut D D2 (cultnet-ts): a send Node refuses never escapes a timer or a receive handler
Node reports a failed UDP send two ways: it throws for an argument it
refuses (port 0, which any spoofer can claim as a source) and passes a
system failure to the callback later. sendRudpDatagram routes both to one
failure handler, and isPermanentSendError classifies with the permanent set
the other runtimes use (EMSGSIZE, EINVAL, EAFNOSUPPORT, and Node's own
ERR_SOCKET_BAD_PORT for port 0).

- The socket transport's receive and resend paths hand a failure to that
  handler: a datagram that can never be sent ends the session (goodbye
  names the error code, never its message); any other failure is emitted
  on "error" as the socket always reported it. Caller-directed sends still
  throw to the caller. A resend loop stops at the generation it ended.
- The operation server starts no session for a Connect whose Accept can
  never be sent (counted as dropped), ends a stored peer whose datagram
  can never be sent, and counts other failures in sendFailures.
- CultNetRudpSession.endUnsendableSession, as in the other runtimes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:03:01 +02:00
Meta Vens
b4cee00af2 Ack Cut 3: a server's new generation does not inherit acknowledged bytes
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>
2026-09-30 14:46:38 +02:00
Meta Vens
1833df58d4 cultnet-rs: a direct send measures the window after reclaiming expired sends
`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>
2026-09-30 14:46:20 +02:00
Meta Vens
93f1a50895 Merge origin/main into hands/ack-cut3: Cut 3 and D Soul rulings, RUDP client bind
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:44:10 +02:00
Meta Vens
e079ba1ad3 cultmesh-kotlin: the bind self-test observes a wildcard, not its family
A JVM wildcard socket is dual-stack, so a client bound to 0.0.0.0 reports the
IPv6 wildcard as its local address and the self-test's first check failed on
Linux. Assert that a client for a remote endpoint binds a wildcard, and keep the
exact loopback checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:52:26 +02:00
Meta Vens
cf62461643 cultmesh-kotlin: an RUDP client for a remote endpoint binds the unspecified address
The CultMesh RUDP client entry points defaulted bindHost to 127.0.0.1. On
Windows a socket bound to loopback cannot send off the host (os error 10051),
so a default client could not reach a remote endpoint.

cultNetRudpClientBindAddress owns the rule: loopback for a loopback endpoint,
otherwise the unspecified address of the endpoint's family (0.0.0.0 or ::).
bindHost defaults to null on every client entry point, which uses the rule. An
explicit bindHost still wins; a CultMesh server still binds 127.0.0.1 unless
told otherwise. The self-test pins all three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:49:31 +02:00
Meta Vens
9954652266 cultnet-py, cultmesh-py: an RUDP client for a remote endpoint binds the unspecified address
create_rudp_schema_transport and CultMesh.create_rudp_client (and create_client
and connect_client above them) defaulted bind_host to 127.0.0.1 on an AF_INET
socket. On Windows a socket bound to loopback cannot send off the host (os
error 10051), so a default client could not reach a remote endpoint.

cultnet-py now owns the rule in rudp_client_bind_host: loopback for a loopback
endpoint, otherwise the unspecified address of the endpoint's family (0.0.0.0
or ::). bind_host defaults to None on the client paths, which use the rule; the
socket family follows the bind host. An explicit bind_host still wins; a
CultMesh server still binds 127.0.0.1 unless told otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:44:06 +02:00
Meta Vens
b68ddfe357 cultnet-ts, cultmesh-ts: an RUDP client for a remote endpoint binds the unspecified address
CultMesh.createRudpClient defaulted bindHost to 127.0.0.1 on a udp4 socket, and
invokeCultNetOperation bound 127.0.0.1 unconditionally. On Windows a socket
bound to loopback cannot send off the host (os error 10051), so neither could
reach a remote endpoint.

cultnet-ts now owns the rule in rudpClientBindHost: loopback for a loopback
endpoint, otherwise the unspecified address of the endpoint's family (0.0.0.0
or ::). The operation client and the CultMesh client both use it, and the
CultMesh socket family follows the bind host. An explicit bindHost still wins;
a CultMesh server still binds 127.0.0.1 unless told otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:41:44 +02:00
Meta Vens
88d2d72405 cultnet-rs: an RUDP client for a remote endpoint binds the unspecified address
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>
2026-09-30 13:37:21 +02:00
Meta Vens
aab72f3d47 Ack Cut D D2: a refused send withdraws the fragments the flow window queued behind the failed one
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>
2026-09-30 12:49:46 +02:00
Meta Vens
69acee38e7 Ack Cut D D2 (Python): a refused Accept leaves the peer with the goodbye only
The peer is told its session ended, and no Accept resend follows it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:27:33 +02:00
Meta Vens
ac5c86033b Ack Cut D D2: pin the disconnect, the resend loop, the Python resend, ack and accept paths
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>
2026-09-30 12:25:04 +02:00
Meta Vens
1c71d98c71 Ack Cut D D1 and D2 (Python): a vanished peer is idle, and a send that can never succeed ends only that peer's session
D1: on Windows the closed port of a vanished peer is reported by the next
recvfrom as ConnectionResetError (WinError 10054), and the RUDP thread returned
on it. It is now idle, as in the Rust document server, the Rust hub and the C#
listener. Both tests fail on Windows without the fix and pass with it.

D2: is_permanent_send_error (EMSGSIZE, EINVAL, EAFNOSUPPORT and their Winsock
equivalents) separates a datagram that can never be sent as built from a lost
one. The server ends that peer's session with a goodbye whose reason names the
error and never the thread; transient failures stay counted losses.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:19:47 +02:00
Meta Vens
64ed64a894 Ack Cut D D2 (Rust): a send that can never succeed is the caller's error inside a send and ends only that peer's session inside a poll
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>
2026-09-30 12:19:46 +02:00
Meta Vens
436b423762 Ack Cut 3 F1: the acknowledged-bytes tests pin pruning on the ack that closes the gap
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>
2026-09-30 12:12:23 +02:00
Meta Vens
0daf6f4cc0 Ack Cut 3 F1: Rust pins that an expired lowest packet releases the acknowledged bytes above it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 12:07:28 +02:00
Meta Vens
d7eacd1011 Ack Cut 3 F1: the 4 MiB flow bound counts acknowledged bytes above the lowest unacknowledged sequence
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>
2026-09-30 12:04:19 +02:00
Meta Vens
2125635dff Ack Cut D: the document-server send-failure tests take initial_sequence as an Option
Ack Cut 1c made CultNetRudpSocketTransportOptions.initial_sequence optional; the
new client helper still passed a bare u32.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:26:51 +02:00
Meta Vens
0996fc96f5 Ack Cut 3: the hold-buffer test acknowledges the gap by hand
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>
2026-09-30 11:07:30 +02:00
Meta Vens
b8c00117b3 Merge origin/main into hands/ack-cutD 2026-09-30 10:58:23 +02:00
Meta Vens
ede87f7423 Ack Cut 3: the flow-window tests shake hands the way Cut 1 requires
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>
2026-09-30 10:57:53 +02:00
Meta Vens
8c71b5c6dd Merge origin/main into hands/ack-cut3
# Conflicts:
#	packages/cultnet-ts/src/rudp.ts
2026-09-30 10:52:49 +02:00
Meta Vens
05b5705d69 Merge ack Cuts 1, 1b and 1c: one watermark, the write belongs to its session, a new generation is recognised
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>
2026-09-30 10:28:05 +02:00
Meta Vens
c45edf5858 Ack Cut 1d batch 2 (Python): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
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>
2026-09-30 09:53:06 +02:00
Meta Vens
9c562adaf2 Ack Cut 1d batch 2 (TypeScript): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
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.

The cultmesh-ts document server has a test that it does not acknowledge by name a reliable packet its session did not receive.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:53:06 +02:00
Meta Vens
7b46b4c481 Ack Cut 1d batch 2 (Rust): an Ack does not retire a Connect awaiting its Accept; the fresh attempt jumps by the receive window less one
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>
2026-09-30 09:53:05 +02:00
Meta Vens
e56c96d6be Ack Cut 1d (Python): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
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>
2026-09-30 09:11:51 +02:00
Meta Vens
ba873d04c4 Ack Cut 1d (TypeScript): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
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.

The cultmesh-ts document server acknowledges what the session received, not every reliable packet by name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 09:11:51 +02:00
Meta Vens
e02d027019 Ack Cut 1d (Rust): stale generations are duplicates, a stale Connect is answered not obeyed, sessions draw their own sequence
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>
2026-09-30 09:11:51 +02:00
Meta Vens
29e60353f0 Merge media FEC Cut 3: one Reed-Solomon codec for audio and video
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>
2026-09-30 09:05:11 +02:00
Meta Vens
4d93dc858e Media wire tests: hold an unfragmented media packet to the ceiling
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>
2026-09-30 08:29:05 +02:00
Meta Vens
df892e40e3 Ack Cut 1c tests (cultmesh-rs): the budget test sets a snapshot limit that fits its session budget
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:20:26 +02:00
Meta Vens
7c149bf0c8 Media wire tests: fix helper name clash and move fixtures to the last-pts deadline rule
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>
2026-09-30 08:14:22 +02:00
Meta Vens
1bbc0ace69 Media wire: the encoder refuses a record over the ceiling too
A producer that builds an oversized record without going through the FEC
policy would otherwise learn only when the receiver silently dropped it.
encode_media_wire_record now refuses anything over
GAMECULT_MEDIA_MAX_WIRE_BYTES, with the decoder's message. The decode ceiling
test pads a valid record by one byte to stand in for a peer that sent one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:11:26 +02:00
Meta Vens
51e9a550f9 Media wire: bound every record by one ceiling, and validate audio parity against its last pts
Removing the 720-byte shard cap left the decoder with no per-record size
bound: RUDP reassembles up to 16 MiB and the media channel set no cap, so a
4 MiB parity record decoded and recovery work scaled with it.

GAMECULT_MEDIA_MAX_WIRE_BYTES (64 KiB, one maximum UDP datagram, ~7x a jumbo
frame) is now the ceiling. decode_media_wire_record refuses a larger record,
the RUDP session caps the media channel to it on send and on reassembly (and
the profile advertises it), and MediaFecPolicy::validate refuses a
max_wire_bytes above it so no producer can promise a record the decoder drops.

validate_audio_parity_record now checks the deadline against the last slot's
pts, not the base, so recovery can no longer produce a packet that violates
deadline_ticks >= pts_ticks.

Tests: ceiling +-1 at decode for audio and video parity; jumbo record accepted;
session send and reassembly on the media channel only; profile advertisement;
the audio deadline boundary and recovery refusal; a policy over the ceiling
refused; a budget at the ceiling end to end; audio parity larger than every
packet it protects (the budget check relies on it). MediaFecPolicy docs tell
callers to build by struct update from STANDARD.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:10:07 +02:00
Meta Vens
3f942a57e7 Cut 4 Soul 4d: a skipped entry is relayed as the primary sent it; probes, wire cases and the closed-enum rule committed
F1: RecordUnappliedEntry keeps the primary's wire entry on the in-memory log
entry (internal Relayed) and ToLogEntryMessage returns it through the same
normalisation the store uses, so a replica with no store no longer relays a
skipped put as a delete; memory and disk relay identically.
Soul's 4d probes are committed (chained replica keeps the row, key-prefixed
reads serve only owned keys, primary-all serves everything; the two
measurements are explicit) with a memory/disk relay-parity test.
Rust: captured-bytes cases for unowned_schema and variant_unsupported (with and
without a subscription).
The error-code set is stated as a closed enum in the schema description (both
copies) and in cultnet-distributed-database.md: adding a code is one
coordinated change across runtimes, since Rust and TypeScript refuse an unknown
code. The Soul 4c probe file's header says what it guards.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:03:09 +02:00
Meta Vens
f66fd3d714 Ack Cut 1c tests (cultmesh-rs): the restarted client's message is the same size as the first
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 07:03:27 +02:00