The pending-fragment bound evicted the first non-reliable set in insertion
order in TypeScript and Python, so a set actively receiving could be the
victim. Rust already took the least recently touched set. TS and Python now
track a per-set touch counter and match Rust. A test in each runtime pins the
scenario: bound full, the oldest set receives another fragment, a new set
forces one eviction, and the oldest set still completes.
Session receive in all three runtimes now documents that a refusal error ends
the session and the caller must end it, since the refused sequence is already
recorded and would be acknowledged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
session_and_hub_reject_configured_memory_bounds expected a reliable
fragment set to be evicted at a bound of one pending set. That is the
loss the cut removes: the second reliable set is now refused and nothing
is evicted. Unreliable eviction stays covered in tests/cultnet.rs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rust, TypeScript and Python receivers evicted the stalest fragment set
when the pending bound was reached, including sets whose fragments had
already been acknowledged. The sender never resends an acknowledged
fragment, so the frame was lost while the sender saw it delivered.
Eviction now picks the stalest set holding no reliable fragment. When
every pending set is reliable the packet is refused, as every other
refusal is. A conforming sender keeps at most 32 reliable packets in
flight against a bound of 64 sets, so it cannot reach the refusal.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rust, TypeScript and Python receivers evict the stalest fragment set when
the pending bound is reached, including sets whose fragments were already
acknowledged. These tests fail until eviction skips reliable sets and a
bound full of reliable sets refuses the packet.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Also adds a swept-after-handler test for the operation service and states
the Rust receipt comment exactly: only a peer Disconnect invalidates receipts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repeated-Connect answer applies its acknowledgement field in every runtime;
no test failed when a runtime stopped doing so. The TS replaced-peer test now
lets client A receive its Accept before A sends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Disconnect left the session's pending writes and scope alone, and the
transport's Invalidated status leaned on disconnect_reason, which connect()
clears. After a reconnect the receipt returned to Pending and could become
Acknowledged in the new session. The session now rotates its scope and drops
what it still owed on Disconnect, so the session is the one owner of
Invalidated and the transport override is gone.
A flush records the session scope it started in and fails when a Connect that
replaces the peer rotates it, instead of resolving on the emptied queue.
Tests: Disconnect-then-reconnect receipt, stale end reason after a new Connect
is accepted, flush across a replaced peer, and a repeated Connect's ack field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server-mode transport treated a Connect from a new endpoint as a repeated
Connect once the session was connected: the new client got an Ack and never an
Accept, and the old session never ended. Only a Connect from the accepted
endpoint repeats; another endpoint resets the session and gets its Accept.
A flush recorded nothing about the session it started in, so a disconnect
listener that reconnected cleared the end reason before the 5 ms poll saw it.
Sessions now carry a generation; a flush fails if its generation ended.
The operation service's idle sweep deleted a session whose handler was still
running, losing the response. A session with a handler running, or one that
finished a handler within the idle window, is not idle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hub test counts replaced sessions, since a fresh session repeats the same
Accept sequence and the distinct-sequence count could not see it. The TS test
requires an Ack, not a new Accept, once the Accept was acknowledged. A peer
Disconnect is pinned as an ended session for flush and receipts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flush now rejects for writes an ended session forgot. acceptConnect on a
connected session delegates to the new answerRepeatedConnect, so a repeated
Connect re-sends the Accept still owed, or an Ack once it was acknowledged,
and queues nothing; the operation service no longer builds a session per
Connect. The operation service dropped sessions never, so 64 abandoned
clients locked every new one out: a session idle for sessionIdleTimeoutMs
(default 30 s, the document server's session_idle_timeout) is now dropped.
A request whose handler rejects ends its session like a transport refusal,
so the invoker learns promptly and a retransmit is not acknowledged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flush_reliable raised nothing for writes the ended session had forgotten; it
now raises ConnectionError with the reason. Server mode answered every
repeated Connect by queueing a fresh reliable Accept; a Connect from the peer
already accepted is now answered with the Accept still owed and queues
nothing, as the Rust hub does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session that refused a packet, or was disconnected, forgot its
unacknowledged writes, so flush_reliable returned success for writes the peer
never acknowledged. The transport now fails the flush with the reason and
reads such writes as Invalidated. Reset no longer reissues sequence numbers or
fragment ids: a late duplicate Connect in server mode reused sequences the
peer had seen. end_refused_session owns the reset-then-goodbye ordering for
the hub, the transport and the document server, which used to end a refusing
session silently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fragment-id-0 test is caught by the session-refusal branch, so a frame that
fails inside the handler needs its own test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server mode claimed the endpoint on the first datagram of any type with the
right connection id, so an orphan Ack made a later real client a stranger. Only
a Connect claims it now. A session that refuses a packet has already recorded
its reliable sequence, so the transport ends it: the session forgets the peer
(so the goodbye does not acknowledge the refused frame), a Disconnect goes to
the peer, disconnect_reason is set, and a server-mode transport forgets its
endpoint. CultNetRudpSession gains reset_peer_state and rejects an
initial_sequence of 0xFFFFFFFF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session that refuses a packet has already recorded its reliable sequence, so
the socket transport now ends it: the session forgets the peer (so the goodbye
does not acknowledge the refused frame), sends a Disconnect, forgets the
endpoint in server mode, and emits disconnect and close. The operation service
did `void handleServerDatagram(...)`, so any rejection was an unhandled
rejection that ended Node; it now drops and counts the packet (packetsDropped),
ends a session that refuses a frame, and holds at most 64 sessions, dropping a
Connect from a new peer beyond that. CultNetRudpSession rejects an
initialSequence of 0xFFFFFFFF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Disconnect a refusing session sent carried the ack of the very frame it
refused, so the sender saw the frame acknowledged. The session forgets its
peer's sequences before building the goodbye, and a server-mode transport
forgets its endpoint so only a Connect can claim it again.
CultNetRudpSession::answer_repeated_connect is the one answer to a repeated
Connect for the hub and the document server: it records the sequence (the
document server's old accept_connect did, and a client that reconnects in one
session depends on it), applies acks, queues nothing, and returns the pending
Accept or an Ack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The document server called accept_connect again for a Connect on an existing
key, queuing another reliable Accept each time: a resend storm, and the full
queue Err that the previous commit turned into a drop. The repeat now re-sends
the Accept still pending, or an Ack once acknowledged, as the hub does. A fresh
session's accept_connect cannot fail under validated options, so its error is
propagated again rather than counted as a drop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CultNetRudpSession::receive records a reliable sequence before reassembly, so
keeping the session after it refuses a frame made the sender's retransmit be
acknowledged and the frame silently lost. The hub and the socket transport now
end that peer's session, send it a Disconnect, and surface the end to the
caller (a Disconnected event, disconnect_reason()). The hub and socket
constructors reject initial_sequence u32::MAX and max_pending_reliable_packets
Some(0), so the accept_connect drops are unreachable. pending_accept_for_resend
becomes public for the document server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A malformed frame, a foreign connection id or a refused packet raised out of
receive_once. They are now counted in stats.packets_dropped and receive_once
returns None. The id is checked before the peer endpoint is claimed, so a
stray arriving first can no longer become the server-mode peer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A foreign connection id, a malformed frame or a refused packet was emitted
as an error event; with no listener that throws and ends the process. They
are now counted in stats.packetsDropped. The id is checked before the peer
endpoint or session state is touched, and an unadmitted first packet in
server mode no longer claims the endpoint. Listener faults still surface as
error events.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Connect storm from a flow that never hears its Accept filled the session
reliable queue and accept_connect returned Err out of poll_once. A refused
Connect, a malformed frame and a packet for an unknown session are now
counted in packets_dropped() and poll_once keeps serving.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A foreign connection id, a malformed frame, a sender no Connect admitted, a
Connect with the peer table full, and any packet the session refuses ended
the transport loop with an Err. Odin died twice this way after nginx reloads
moved its UDP flows (require_connection: connection id does not match).
The hub and the socket transport now drop such packets and count them in
CultNetTransportStats::packets_dropped. Only socket I/O errors leave the
loop. The socket transport checks the connection id before it touches the
peer endpoint or session, so a stray Connect can no longer move the endpoint
or reset the session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul: no defect; independent port of stegu 22434e04d7 matches (3D and 2D), goldens
confirmed independently, value jump 2.2e-6 over 20k crossings, gradient FD 9.7e-6,
every hand mutant killed. 2D follows upstream by ruling. Release follow-ups: rebuild
the Unity CultMath.dll, CHANGELOG entry, pin the two HLSL copies equal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closed after three Soul passes: the double iterate and progress stop hold over
~9000 cases against exact enumeration, zero allocation, deterministic; the stall
rule and fScale are now pinned (all six survivors killed). Ships in the next
CultMath release with Asura. Recorded, outside the documented range: 18/6000
gap misses at cond 1e5-3e5 with A scaled 1e-4..1e4 (solver or reference unknown).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Batches of small boxed problems whose passes end with the KKT test failing and
the cost unmoved (dependent free columns, releases the factor drops, weak-direction
noise-floor problems). They must report Converged, and land within the documented
gap of an exact active-set enumeration. Pins the stall comparison and the cost
scale in Objective, which no test reached before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Seven values change: four snoise(float3) in HlslSemanticsTests and three snoise(float2)
in MathTests (the 2D (0,0) golden is 0 either way). Regenerated from the code after the
continuity probe passed. The tests no longer claim to match Unity.Mathematics, whose
snoise keeps the old constants.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drops the unreproducible floors and detection-scale claims, and the seam-fluke
consistency filter (the spikes it hid were the snoise seam, now fixed and pinned by the
continuity probe). Tightens the tolerances to at least 5x the harness maximum (ridged
0.5 -> 0.05), corrects the point count in the bit-equality test, and caps the ridged
compounding loop so a mutant fails loudly instead of hanging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
With b along the smallest left singular vector, |A^T b| is tiny, so the KKT
tolerance (1e-6 of it) sits below the gradient's rounding noise at cond(A) >=
1e6 and can never be met. The solver must stop on the progress rule, in at most
six passes, never at the cap. This is the path the progress rule exists for;
the earlier fixtures reached it only by accident.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two tolerance behaviours had no observer: the scale taken at the box point
nearest the origin (a 6e-5 release on a 1000-scale problem, in either column
position), and the strict comparisons when the scale and the gradient are both
exactly zero (b = 0 at the origin takes no iteration).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reconcile's withdrawal sent an untyped error with no subscription id and left the
peer holding the rows the subscription had delivered. It now sends removals for
those rows, then a variant_unsupported error whose details carry
field "subscriptionId" and the id (the existing details slots; no new wire key).
The subscribe and per-change paths use the same code. The error schema and the
Rust CultNetErrorCode accept variant_unsupported, so a Rust peer decodes it
instead of refusing an unknown code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The full-step clamp repairs an out-of-box start on a free column, so the
warm-start clamp was invisible to the old test (Stryker: five survivors). The
new table runs one column with b beyond the box and a cap of 0, where the
solver returns the clamped start untouched, across finite, infinite and NaN
starts and finite and infinite bounds.
Workspace junk (including NaN doubles) is now checked on random boxed problems
that release bounds and use the tolerance, with bit-identical x and equal
iteration counts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Measured with Stryker once Solve was actually mutated (21 survivors), most of
them were code that decided nothing:
- The finite-input loops over A and b duplicated the non-finite test on the
accumulated A^T b, which is exactly equivalent (floats cannot overflow a
double product, and NaN/infinity never cancel to a finite value). One check
on each A^T b entry remains.
- The tolerance scale is the gradient at the box point nearest the origin,
computed inline; the max with |A^T b| duplicated it whenever the origin is in
the box, and the scratch vector for it is gone.
- Bound violation is state * gradient, which is zero for a free column; the
three-way ternary duplicated the free-stationarity check.
- The warm-start clamp is Math.Clamp plus a NaN test.
- Workspace slices come off a cursor (Take) instead of hand-summed offsets.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stryker left the refinement step (removed, negated, or subtracted) unobserved,
and it is unobservable for a reason: refinement with a same-precision residual
does not improve forward error, and Cholesky is backward stable, so the
residual the KKT test reads is already at rounding level. Measured on n=24
problems at cond(A) 1e3..1e6, the solver with and without the step gives the
same iteration counts and the same x error against a Gram-Schmidt QR
reference (both limited by cond(A^T A) * eps).
The refinement pass, its dp buffer and the scratch vector are deleted;
WorkspaceLength returns to 4n^2 + 15n. The doc states where the normal
equations stop being optimal (cost gap below 1e-6 |b|^2 through cond(A) 3e5,
about 2e-5 at 1e6, independent of the scale of A) and that a ridge row beyond
that is the caller's policy.
status is now initialised: the unassigned local made Stryker discard every
mutant inside Solve as a compile error, so 255 mutants were never measured.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- n=24 fully free problems at cond 1e2/1e3/1e4, and A x 1e6 with unscaled
bounds, converge in at most two iterations (a regression to cap-spinning
fails on the count, not on a clock).
- b = 0 from a far start reaches the optimum instead of the start.
- Warm-start clamp table, free duplicate column gets no step, parallel columns
at absolute scales 1e3 and 1e-3, near-singular free sets confirm once and
stop, near-collinear optimum at A x 1e6, and zero allocation on the invalid,
converged and cap paths (first and second call).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The working iterate now lives in double inside the workspace (WorkspaceLength
4n^2 + 17n); A, b and the API x stay float and x is converted in at the start
and out at the end. The gradient, the free-stationarity check and the KKT test
all run in the double domain, which removes the float noise floor that made the
free-stationarity re-solve spin to the cap on optimal points (cond >= ~20).
A fixed tolerance is no longer the only stopping rule: after a stationarity
miss or a release, a pass that lowers the double cost by no more than rounding
reports Converged.
The tolerance scale is the larger of the gradient at the origin and at the box
point nearest the origin, so it never depends on the warm start; b = 0 with a
far start is no longer judged converged by its own steepness.
Two clauses were equivalent or unobservable and are deleted: the d <= 0 pivot
test (implied by d <= tol * diag) and the dependent[] exclusion from the
free-stationarity check (the progress rule now terminates it).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The four hard-coded sets become flags, with @file response-file splicing.
A consumer keeps its own .args file instead of committing a patch against
the example. fixture.args carries CultLib own sets; the README documents
the flags. Committed fixture bytes are unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
C#, TypeScript, Python, Rust and CultMesh refuse an unknown
cultcache.store.* header and a fifth record slot, naming the version or the
key and schemaId. The silent slot-skipping and C#'s ignored header are
deleted, and CultMesh's second record decoder is gone. Shared vectors are in
tests/vectors/document-variants-c0/. v1 stores stay byte-identical. At base,
the single-file commit's read-modify-write rewrote a v2 store as v1, which
lost data; it now refuses.
Soul passed it. Cross-runtime interop was run and passes. Three C#
Caching tests fail on Linux at both base and tip: they are Windows
share-mode tests (a FileShare.Read handle blocking delete/replace), which
cannot pass on Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>