Soul's mutant deleting the empty-file arm of write_all_unlocked survived: no
test rewrote a zero-byte file. Without the arm the empty file reaches
store_header and is refused as truncated. The test pushes onto a zero-byte
file and expects a v1 store holding the pushed record.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rewrite read the header of the file it replaces but not whether the file was
a store: C# ReadStoreHeader skipped one value and ignored bytes after it, and
took a non-array value or an array whose first slot is a string that is not a
store header for a headerless legacy file (so did the C# Mesh raw write, which
uses it); TypeScript's pushAll took any non-array value or non-header first slot
for v1; Rust ignored bytes after the array.
The rule, one per runtime: a file is a store on rewrite only when it is exactly
one complete MessagePack array. Bytes after it, a value that is not an array,
and a first slot that is a string but not a header this runtime reads are
refused, and the file is left untouched. An empty file and a legacy envelope
array (first slot not a string, empty included) carry no header and are still
rewritten.
- C#: ReadStoreHeader refuses non-array values, trailing bytes and a non-header
first string, with a NotSupportedException naming which.
- TypeScript: storeHeader (store-format.ts) decides the rewrite header;
@msgpack/msgpack's decode already refuses trailing bytes.
- Rust: store_header(bytes, whole) walks with a cursor; the rewrite path asks
for the whole file, the read paths keep reading as before.
- Python: push_all already reads the whole file (unpackb refuses trailing
bytes, the legacy decoder refuses anything but a list of maps); pinned by a
test.
Tests in each runtime run the same file matrix (truncated, trailing, scalar,
map, string first; empty, empty array, legacy), plus a C# unconditional commit
over a store with trailing bytes and the C# Mesh raw write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, at the end of Admit in CultCache.cs. The branch had added the
IdsInMemoryOnly marking of a load's minted ids before Apply; main had turned
the thread's hold into a Hold carrying Changes and Failures and run the
journals right after Apply. Both are kept, in that order: the marking touches
only the records and runs no user code, then Apply mints the Sequences, the
hold records the changes, and the journals run under the gate.
Id planning and Project do not collide. Ids are planned and applied first,
Resolve (variants, then Project, then the name and index rules) reads the
filled documents, land runs, and only then Apply mints. A Project getter that
throws is caught by the same handler that undoes the id plans, so a refused
admission changes no document and mints nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmp_serde refuses a sequence visitor that stops early, so the prefix-only header read the
previous commit introduced could not decode any store. store_header is now one visitor: the
first slot is the header (None when it is not a string, the legacy envelope array), the rest
is walked, and a truncated file is an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
store_header returned None for any bytes it could not walk, so push_all read a truncated
variant store as legacy and overwrote it v1. It now proves the file is one complete
MessagePack value first (an error otherwise, the rewrite refuses and leaves the file), then
reads the header from the prefix; the reader's own header read no longer walks to the end.
Co-Authored-By: Claude Opus 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 behaviour was already a refusal that leaves the file untouched; now a test fails if a rewrite
ever reads the disk header less strictly than a load does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
push_all reads the file and reuses store_header and readable_store_format, the same pair a load
uses. The 96-byte prefix parser it replaces misread a str16 header and any str8 header past the
prefix as v1 and overwrote a store it could not read. Pins both encodings refused with the file
untouched.
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>
push_all overwrote a v9, variant-v2 or variant store with a v1 file, dropping its variants; TypeScript and Python
refuse. The rewrite header now comes from the file's own prefix and shares one readable-format rule with the
decoder. TypeScript pullAll resets its header when the file is gone, empty or legacy, as Python does.
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>
TS pushAll wrote the header its instance last read (v1 when fresh) without reading the
file. It now reads the on-disk header first, as push and delete do through pullAll. Python
resets its remembered header when the file is gone or empty, so the disk decides.
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 store that can hold element ids declares cultcache.store.v3. These runtimes pass payload
bytes through, so they read it and write back the header they found; they do not decide
when a document carries ids. Vector: tests/vectors/document-variants-c2a/v3-base.msgpack.
Co-Authored-By: Claude Sonnet 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>
The C# cultcache interop peer gains an object-list document (a deck of marks).
C# writes it with cache-minted ids; TypeScript, Python and Rust read the ids as
ordinary members, and TypeScript-written ids survive a C# read.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CultWitnessContractPin, CultWitnessArtifactEntry and CultWitnessTimingEntry get
an id slot. The JSON schema contract and the Python schema copy gain an optional
id per element, and the Python and TypeScript fixtures carry and assert ids.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[CultElementId(nameof(Member))] declares an id derived from that member's
invariant text, never random, so content-addressed elements keep encoding to the
same bytes. CultMeshCdnChunkRef derives its id from Offset; PackArtifact sets
it, the Rust pack_content writes it, and the committed content vectors are
re-cut once so both runtimes carry it. Engine tests drive Problems and Assign
directly (InternalsVisibleTo the Caching tests).
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>