Skip to content

harness(rg3): fix K2 baseline block-skip race + empty-string MOCK_TRACE guard#169

Merged
dzhelezov merged 2 commits into
mainfrom
harness/k2-baseline-race-fix
Jul 13, 2026
Merged

harness(rg3): fix K2 baseline block-skip race + empty-string MOCK_TRACE guard#169
dzhelezov merged 2 commits into
mainfrom
harness/k2-baseline-race-fix

Conversation

@dzhelezov

Copy link
Copy Markdown
Collaborator

What

Two harness-only fixes in the realtime chaos mock (harness/chaos/realtime/). No portal/ product code changes.

1. K2 baseline block-skip race (mock-portal.mjs, handleBlocks)

K2 is the only scenario with turnDelayMs: 450. That inter-block gap lets the client open an overlapping /stream connection (e.g. on a finalized-head advance) while a prior turn is still mid-write. Both handleBlocks turns read+wrote the shared module-level streamCursor, so their block-number arithmetic interleaved and emitted a skipped number (100 → 101 → 103). The client correctly fataled on the corrupt sequence (streamed block 103 has an unknown parent … skipped block) — a mock defect, not a product one: a real Portal serves each /stream from its own fromBlock independently.

Fix: seed a per-connection localCursor for the block-number arithmetic; the shared streamCursor is only mirrored best-effort for the fromBlock-less default of the next request. Adds MOCK_TRACE-gated request-interleave instrumentation used to pin the mechanism.

2. Empty-string MOCK_TRACE guard (mock-portal.mjs, trace)

orchestrate.sh forwards MOCK_TRACE via ${MOCK_TRACE:-}, so a caller that never sets it reaches node as an empty string, not undefined. The === undefined guard let that empty value through to appendFileSync(''), which throws ENOENT — returned as a 500 on the very first /finalized-head probe and surfacing as a spurious cannot establish finality boundary fatal for every non-tracing run. Guard on !TRACE_FILE so unset and empty both mean "tracing off"; a real trace path is unaffected.

Evidence

  • Race fix: K2 subset ×15 kills → 15/15 clean resume, 15/15 byte-identical digest match, 0 duplicate finalized rows, kill blocks spread 103–110 (mid-stream, HIGH fidelity). No 102-skip in the post-fix trace (clean sequential 105..112).
  • Guard fix: direct /finalized-head probe — MOCK_TRACE=""500 {"error":"ENOENT: … open ''"} pre-fix, 200 post-fix; valid MOCK_TRACE path still writes the trace file; fully-unset stays 200.
  • Biome (pinned 2.5.2) clean on the changed file.

The K2 midstream-kill semantics are preserved (killAt.block still fires mid-stream; kill spread over the stream interior).

dzhelezov and others added 2 commits July 13, 2026 10:02
…cursor)

The K2 baseline (turnDelayMs:450) aborted at K2-midstream: the mock's /stream
delivered 100→101→103, skipping 102, so the product correctly fataled on a
corrupt sequence (portal-realtime.ts:1101 "unknown parent").

Root cause (mock artifact, NOT a product bug): handleBlocks iterated block
numbers off a MODULE-LEVEL `streamCursor`. K2's 450ms inter-block delay lets the
client open an overlapping /stream (a legitimate reconnect on the finalized-head
advancing 100→103 mid-stream). Two concurrent handleBlocks turns then read+wrote
the one shared cursor across their delays and interleaved: conn1 wrote 102 and
set cursor=102, conn2 woke, read cursor=102 and wrote 103 — skipping 102 on
conn2, the connection the client consumed. Fast classes (K3) never hit it: the
window is too small to overlap. Confirmed via MOCK_TRACE request-interleave.

Fix: seed a per-connection `localCursor` from the request's fromBlock and do all
per-block arithmetic against it; a real Portal serves each /stream connection
independently. The shared `streamCursor` is still mirrored best-effort for the
fromBlock-less default of a subsequent request. Also adds MOCK_TRACE-gated
(no-op when unset) request/cursor tracing for future harness debugging.

No product code under portal/ is touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
orchestrate.sh forwards MOCK_TRACE via `${MOCK_TRACE:-}`, so a caller that
never sets it (the acceptance capstone) reaches node as an empty string, not
undefined. The `=== undefined` guard let that empty value through to
appendFileSync(''), which throws ENOENT — returned as a 500 on the very first
/finalized-head probe and surfacing as a spurious "cannot establish finality
boundary" fatal on every non-tracing run (i.e. the whole capstone). Guard on
`!TRACE_FILE` so unset and empty both mean off; a real trace path is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dzhelezov dzhelezov merged commit 181e48b into main Jul 13, 2026
14 checks passed
@dzhelezov dzhelezov deleted the harness/k2-baseline-race-fix branch July 13, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant