← Priority list

Handoff: UrlResolver saturation flake — negotiator fix split to PR, Noise-handshake residual + version reconciliation remain

Standing request (verbatim intent): *"keep working (flakey test skill) and merging until UrlResolver goes consistently green. Ensure codex does an adversarial review of each change before we merge."* Then, mid-effort: *"let's split it now. Create a PR and link it to me. Create a handoff for the remaining work/issues."*

hf-2026-07-15-urlresolver-saturation-flake-noise-handshake-residual

Edit handoff

Add dependency

Complete this handoff

Moves it out of every priority list and into ArchiveArea.

Handoff document

Markdown

Handoff: UrlResolver saturation flake — negotiator fix split to PR, Noise-handshake residual + version reconciliation remain

Written: 2026-07-16 (UTC). Effort began 2026-07-15.

⚠️ RE-VERIFY BEFORE ACTING. Every state claim below is a write-time snapshot and goes stale. Re-check before doing anything:

  • PRs: gh pr view <n> --repo <owner>/<repo> --json state,mergeStateStatus,statusCheckRollup,mergedAt
  • Published artifacts: curl -s -o /dev/null -w "%{http_code}" https://kotlin.directory/<group-path>/<artifact>/<ver>/<artifact>-<ver>.pom
  • UrlResolver main: gh api repos/CodexCoder21Organization/UrlResolver/commits/main --jq '.sha[0:9]' A fresh query always wins over this document.

Mission summary

Standing request (verbatim intent): "keep working (flakey test skill) and merging until UrlResolver goes consistently green. Ensure codex does an adversarial review of each change before we merge." Then, mid-effort: "let's split it now. Create a PR and link it to me. Create a handoff for the remaining work/issues."

This handoff covers the one flake that is fixed-but-not-fully-landed: stressTestBootstrapServiceResyncDoesNotGiveUpUnderSaturation in UrlResolver. It was root-caused to two distinct upstream libp2p defects. The first (multistream Negotiator close-race) has been split into its own PR. The second (a rarer Noise-handshake stall under extreme dial saturation), the downstream protocol/version reconciliation, and the resolver-side pin/recovery PR all remain.

What was found and done (the chain)

  1. Reliable local reproduction (verified). At the fixed pin, stressTestBootstrapServiceResyncDoesNotGiveUpUnderSaturation failed ~40–60% of runs locally (operator loop 3/8; codex baselines 3/8 and 2-CPU-amplified up to 3/5). Every failure had one deterministic signature: assertion at tests/stressTestBootstrapServiceResyncDoesNotGiveUpUnderSaturation.kt:209 ("PERMANENTLY failed to learn 1 service"), accompanied in node logs by [PEX-EMPTY] re-pull of configured bootstrap … could not complete and a libp2p multistream handler-removal exception during stream negotiation. No OOMs, no env-acquisition errors.

  2. Root cause #1 — multistream Negotiator close-race (verified, fixed, PR open). Under many concurrent stream negotiations to one peer (52 providers → one shared relay), a selected protocol's setup can synchronously destroy a closing child stream's pipeline while the Negotiator's terminal-close callback is still in flight; the second cleanup threw NoSuchElementException removing an already-removed handler, aborting negotiation and silently dropping the provider's register-time peer exchange (relay never learns the service). Fix = make Negotiator handler removal idempotent. Split into jvm-libp2p PR #26 (base develop), with a new MultistreamTest case. This fix alone took the flake from ~40–60% → ~8% (11/12 clean).

  3. Root cause #2 — Noise-handshake stall under saturation (hypothesis, fix in progress, NOT landed). The residual ~1/12 shows dial failures stuck still-handshaking>Xms. Codex's working hypothesis: the Noise read timeout is being measured across time when no outbound handshake frame is actually awaiting a remote response, so under extreme concurrency it trips spuriously. The direction: measure the Noise read timeout only while an outbound handshake message is genuinely awaiting its remote response. This is not yet verified to a clean 12/12 — it hit 11/12 and occasionally 12/12 but never a statistically-confident clean pass before the run was stopped to split out fix #1. Treat as hypothesis, not proven.

  4. Resolver-side recovery (verified as part of the 11/12). On branch fix/saturation-resync-confirmed-pex (commit a7311b12d, pushed): src/foundation/url/resolver/UrlResolver.kt gains PeerExchange-driven relay-reservation recovery (wakes relay-reservation recovery when PeerExchange proves a configured relay candidate's transport is healthy) plus a formatThrowableChain dial-failure cause-chain diagnostic. It also bumps protocol pins to 0.0.362. Not PR'd yet because of version skew (below).

  5. Protocol version skew (verified, blocks the resolver PR). While this work was in flight, a parallel lane published protocol 0.0.363/0.0.364 (adding a "bounded acknowledgement response projection"). The committed WIP pins 0.0.362, which is behind what UrlResolver main now expects — pinning it would regress the parallel lane. Codex was reconciling to 0.0.365 (0.0.364 base + negotiator fix + Noise fix). Both 0.0.362 and 0.0.365 are already published to kotlin.directory — but ⚠️ a higher version number is NOT automatically a content superset; verify 0.0.365's actual contents before trusting it (see the parallel-lane-version-content-skew gotcha below).

  6. A codex run died on the model-capacity ceiling (~1.3M tokens / ~2h). Its work was preserved via WIP commits and the run relaunched. Mentioned so the next driver expects it and preserves-then-relaunches rather than losing work.

Relevant PRs / branches / artifacts (write-time state — RE-VERIFY)

| Item | State @ write | Link | |---|---|---| | jvm-libp2p negotiator fix (the split PR) | OPEN, mergeStateStatus CLEAN | jvm-libp2p PR #26 | | UrlResolver resolver-side recovery + 0.0.362 pins (WIP, pushed, no PR) | branch pushed, head a7311b12d | fix/saturation-resync-confirmed-pex on UrlResolver | | UrlProtocol snapshot-15 + protocol 0.0.362 (WIP, pushed, no PR) | branch pushed, head 0717f12 | fix/multistream-negotiator-close-race on UrlProtocol | | Noise-handshake residual (in-progress fix) | local git stash only — exported as patch artifact | artifacts/2026-07-15-noise-handshake-residual.patch | | Codex residual notes | ephemeral capture | artifacts/2026-07-15-codex-noise-residual-notes.txt |

Already merged this session (context, not remaining work): UrlResolver PR #769, UrlResolver PR #776, UrlProtocol PR #360, UrlProtocol PR #362, jvm-libp2p PR #25. UrlResolver main was at b4eeb7c7b.

⚠️ The 0.0.365 pin-bumps in UrlProtocol and UrlResolver are in local git stashes only (residual-0.0.365-pins in each of /code/workspace/UrlProtocol and /code/workspace/UrlResolver). They are regenerable mechanical pin bumps, but they live only on the /code box — do not rely on them surviving.

Next steps (ordered — re-verify current state first)

  1. Re-verify everything in the table above (PR #26 state, whether main advanced past b4eeb7c7b, whether a newer protocol version than 0.0.365 now exists, whether the parallel lane's 0.0.364 work has merged/moved).

  2. Shepherd jvm-libp2p PR #26 to green + merge (needs its own review; adversarial-review it per the standing instruction before merging). It is self-contained (Negotiator + MultistreamTest) and independently valuable.

  3. Finish root cause #2 (Noise-handshake residual) under fix-flaky discipline. Restore the stashed WIP from artifacts/2026-07-15-noise-handshake-residual.patch (touches libp2p/src/main/kotlin/io/libp2p/security/noise/NoiseXXSecureChannel.kt, .../security/noise/NoiseHandshakeOffEventLoopTest.kt, and a NoiseHandshakePathHarness "saturated" mode: taskset -c 0,1, SATURATED_DIALERS=120, SATURATION_SPINNERS=32). First reproduce the residual reliably (≥50%) with an amplified harness, then confirm/refute the "measure the read timeout only while genuinely awaiting a remote handshake frame" hypothesis, fix it upstream in jvm-libp2p, and verify the reproducer flips to a clean 12/12 (statistical confidence, not one lucky pass). Have codex gpt-5.6-sol do the implementation; do an adversarial review before merge.

  4. Publish the reconciled artifacts. After #26 (and the Noise fix) merge: publish a new jvm-libp2p snapshot from merged develop; then rebuild+publish a protocol version that is a true content superset of current UrlResolver-main's protocol (0.0.364's bounded-ack projection + the negotiator fix + the Noise fix). Verify contents, not just the version number.

  5. Land the resolver-side PR. Rebase fix/saturation-resync-confirmed-pex on latest UrlResolver main, bump the protocol pins from 0.0.362 to the reconciled version, keep the UrlResolver.kt relay-reservation recovery + formatThrowableChain diagnostic, re-verify stressTestBootstrapServiceResyncDoesNotGiveUpUnderSaturation 12/12 locally, then open the UrlResolver PR and shepherd it green through the merge queue.

  6. Re-measure UrlResolver main's flake rate after the chain lands, to confirm the saturation flake is actually gone.

  7. Separate still-open flakes (task-tracker #12, not part of the above):

    • testRelayRestartSameIdentityReattach — failed on the b4eeb7c post-merge main run: after a relay restarts with the same identity, a fresh client cannot resolve the NAT-ed provider within the 30s budget (Both HTTP relay forwarding and direct libp2p connection failed … TimeoutException, ~54.4s). Not yet reproduced locally or fixed.
    • testHealthySameTierRelayWinsBeforeLowerTier (relay tier-selection) — documented, not reproduced.

Reusable / operational knowledge

  • fix-flaky discipline (non-negotiable): reproduce ≥50% before fixing; fix at the owning layer (these are jvm-libp2p upstream, not resolver-downstream); verify a clean 12/12 before PR; never inflate the 75s window, add sleeps, reduce the 52-provider count, or weaken the line-209 assertion.
  • codex capacity ceiling: codex gpt-5.6-sol dies around ~1.3M tokens / ~2h with ERROR: Selected model is at capacity. Preserve work with WIP commits before it hits that, and relaunch from the committed state with a "do not redo finished work, continue from here" brief.
  • codex over-measurement trap: codex can loop on hitting an exact ≥50% amplified gate instead of diagnosing. When the failure signature is already deterministic, a re-brief ("repro is established — root-cause now, do not re-measure") is the fix.
  • Parallel-lane version-content skew: with multiple sessions publishing protocol branches, a higher artifact version is not necessarily a superset of another lane's fixes. Before pinning/deploying a version, verify its contents (the specific fix is present), not just that the number is larger.
  • Publishing protocol: coursier launch community.kotlin.maven.artifact.publishing:community-kotlin-maven-artifact-publishing:0.0.5 -r https://kotlin.directory -- --workspace /code/workspace/UrlProtocol --buildrule foundation.url.protocol.buildMaven --api-url https://api.kotlin.directory/upload --force. Check publish with the .pom HTTP code (200 = present).
  • Local repro command: cd /code/workspace/UrlResolver && ./scripts/test.bash --test stressTestBootstrapServiceResyncDoesNotGiveUpUnderSaturation (compile-heavy, ~3 min/run).
  • Do NOT put pkill/pgrep -f patterns in codex prompts or in your own kill loopspgrep -f "codex exec" self-matches your own shell (the footgun bit this session). Kill codex by explicit PID, or match the actual node+codex process.
  • PlanRepository merge: auto-merge is off; enqueue via GraphQL enqueuePullRequest, poll state until MERGED; never --delete-branch.