The put's flush is bounded by the result window, so a 25 ms flush and 100 ms window made a slow
ack on a loaded forge runner surface as local_send_failed instead of the window_elapsed the test
checks. Drop the flush override and give the window room.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
"server mode admits a pinned client that restarts on a new port" asserted the Accept arrived after sleep(100) and failed on the forge runner (ok(accept) undefined, run 894). It now polls for the packet or frame it asserts on, up to 2 s; no assertion changed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Vectors admittedMessageIdDuplicates carry [invalid,valid], [valid,invalid], [valid,valid]. Decoders that see the
duplicate (C#, Rust) must refuse all three; map-collapsing decoders (Python, TS, Kotlin) judge the last key.
Python, TS, Kotlin and Rust already behave as declared; C# fails: it accepted [valid,valid].
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Failing before the fix: C# 1 (duplicate messageId, second is U+200B), TypeScript 1 (a + lone surrogate),
Kotlin 1 (a document payload string with a lone surrogate no longer decodes). Python and Rust already pass
the new vectors. The range probes read the generated write-admitted-message-id-ranges.json and pass on the
correct tables; they exist to fail when any range is deleted or narrowed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raw compare. std is_numeric for N; embedded letter (Unicode 15.1) and Default_Ignorable (16.0) range tables.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raw compare. JDK Character categories for L/N, an embedded Default_Ignorable range table (Unicode 16.0).
The MessagePack string reader now refuses invalid UTF-8 instead of substituting U+FFFD.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Raw compare, no trim. The platform's regex property tables carry both properties; no table embedded.
D800 vector now driven from wireHex.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pins Heimdall's rule: a write_admitted messageId needs a Unicode letter or number and no
Default_Ignorable_Code_Point, compared raw. Shared vectors plus a runner test in TS, Python,
Kotlin, C# and Rust. No decoder changed; the refuse vectors fail.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The replay runner handles the sendFailed event, fails on an event key it does
not know and on a vector count the file did not declare. WriteTracker settles a
write sent after sessionEnd as session_ended. Source ids on a raw document
record are dropped when blank by Unicode White_Space (isBlankText), not Kotlin's
isNullOrBlank, which keeps U+0085 and drops U+001C to U+001F; the vector file's
JSON reader skips JSON whitespace only.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
New scenario vectors: id-less error.v1 with one and with two writes unanswered,
sendFailed, an id-less error after the window, a receipt one ms inside the window
and at the exact boundary (late), a send after sessionEnd. The last exposed a
TS tracker leaving such a write pending forever; it now settles session_ended.
The file declares counts, and the TS replay runner fails on a count it did not
run or an event key scenarios.semantics does not name.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
validateBinaryMessageRoot, requireNonEmptyString and requireOptionalStringArray
used String.prototype.trim(), which takes U+FEFF and misses U+0085, so the TS
decoder and the servers disagreed on what names nothing (write-receipt rule 5).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A repeat of a packet the session already took, and a packet too far ahead to be
remembered, delivered no frame and so drew the superseded-write refusal again on
every send, breaking write-receipt rule 1. The refusal now applies only to the
first packet the session takes for a sequence; CultNetRudpSession::has_received
exposes that test.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
cargo-mutants on the previous commit missed two mutants: `&&` to `||` in the
superseded-packet check (a packet that also releases another write hid it) and
`<` to `<=` at the half-space bound of sequence_at_or_before. A test and two
assertions kill them; the rerun catches 14 of 15 (1 unviable).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Superseded packet: a reliable-sequenced packet that arrives after a newer one
delivers no frame, yet is acknowledged and never resent, so its write went
unanswered. The test (red first: only "new" was refused, "old" got nothing)
showed the drop happens in the session below frame decoding, but an unfragmented
packet carries its whole frame, so the server can read the id from the packet
payload. Chosen: refuse it from the packet payload. A superseded fragment of a
larger write has no readable id in the packet, so write-receipt.md now says that
write may go unanswered and the client's unconfirmed outcome covers it.
Ordering: frame.sequence <= packet.sequence is replaced by sequence_at_or_before,
serial arithmetic modulo 2^32. Deviation from the brief: cultnet-rs'
at_or_before carries a 4096-sequence receive window made for duplicate detection;
a frame's first fragment may lie further back than that, so reusing it would
leave such a sequenced write unrefused and applied. The new test pins that, and a
hand mutant with the window fails it. A wrapped sequence cannot reach this
compare through the session today (it is judged a duplicate), so the unit test
pins the helper, not an end-to-end wrap.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A sequenced packet's arrival can release held ordered frames in the same delivery; refuse only the packet's own frame.
heimdall-low-refuse-sequenced-per-frame.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A vector that sets recordKey (zero_width_space_record_key_is_a_name) replaces the harness key, so the admitted case waited for
blank:<name> and timed out against the Rust server, and the refused record-key cases counted applications under a key the server
never saw. Use the key the document carried.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Strict decode of the two receipt frames (and error.v0), hello capability test,
the dedupe content hash and the client settle rules, ported from cultnet-ts.
write-receipt-vectors.json replays 38/38 (constants, contentHash, wire,
schemaInvalid, scenarios, v0 limit_reached); dedupe and blankFields are server
inputs and not run.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The unreadable-frame text is 'message_invalid: the message could not be read' (the servers' wording; the Rust wire never carried decoder text, only its local rejection display). A put that names nothing (blank schemaId/recordKey/storedAt, empty payload, blank present source ids or tags) is refused as unreadable on every connection; the vectors' blankFields section lists the cases and the Rust server tests replay them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>