a73x

2bc64e84

fix: the burst's fifth keystroke aged onto the expiry bound exactly

a73x   2026-08-09 18:43

Commit message
fix: the burst's fifth keystroke aged onto the expiry bound exactly

delaypipe drains one chunk per DELAY_MS serially, so each burst keystroke
ages (DELAY_MS - interval) longer than the one before it. At the old
200ms interval the fifth prediction reached judgment at 600 + 4*100 =
1000ms — predict.expire_after_ms to the millisecond, a coin flip decided
by which of two milliTimestamp() calls in the same poll iteration saw
the boundary first. Measured 4/20 failures on an idle machine; no load
required, and no prediction was ever judged wrong (expired increments
contradicted through the shared abandonAll tail, which is also why the
failure line pointed at the wrong counter — expired is now asserted
first). At 250ms the fifth lands at 800ms: 0/12, margin 201ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

test/e2e.sh
Old New
@@ -1236,7 +1236,7 @@ assert_converged "$OUT.p1" "$SOCK5" "line-mode prediction"
1236 # whole queue was flushed once per round trip. If anyone regresses the 1236 # whole queue was flushed once per round trip. If anyone regresses the
1237 # three-way judgment, this scenario is what says so. 1237 # three-way judgment, this scenario is what says so.
1238 set +e 1238 set +e
1239 { sleep 2; for c in b u r s t; do printf '%s' "$c"; sleep 0.2; done; \ 1239 { sleep 2; for c in b u r s t; do printf '%s' "$c"; sleep 0.25; done; \
1240 sleep 4; printf '\034'; } | \ 1240 sleep 4; printf '\034'; } | \
1241 DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 40 "$MUX" \ 1241 DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 40 "$MUX" \
1242 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ 1242 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \
@@ -1247,10 +1247,24 @@ set -e
1247 echo "e2e FAIL: burst client exited $RC" 1247 echo "e2e FAIL: burst client exited $RC"
1248 cat "$OUT.pb" "$OUT.pb.err" 2>/dev/null; exit 1; } 1248 cat "$OUT.pb" "$OUT.pb.err" 2>/dev/null; exit 1; }
1249 1249
1250 # Five keystrokes typed at 200ms into a 600ms round trip: three are always 1250 # Five keystrokes typed at 250ms into a 600ms round trip: three are always
1251 # outstanding at once. `made` is asserted too, so two keystrokes arriving in 1251 # outstanding at once (at t=600, when the first is confirmed, the keys typed
1252 # one read (which would be suppressed as a multi-byte chunk, not predicted) 1252 # at 0, 250 and 500 are all in flight). `made` is asserted too, so two
1253 # fails loudly instead of quietly weakening the test. 1253 # keystrokes arriving in one read (which would be suppressed as a multi-byte
1254 # chunk, not predicted) fails loudly instead of quietly weakening the test.
1255 #
1256 # The interval is 250 and not 200 because of a bound that has to be COMPUTED
1257 # rather than eyeballed. delaypipe delivers one chunk per DELAY_MS serially
1258 # in EACH direction, so keystrokes closer together than DELAY_MS queue, and
1259 # each one ages (DELAY_MS - interval) longer than the one before it:
1260 #
1261 # age at which keystroke k is confirmed = 2*DELAY_MS + k*(DELAY_MS - interval)
1262 #
1263 # At interval=200 the fifth keystroke lands at 600 + 4*100 = 1000ms, which is
1264 # exactly predict.expire_after_ms: a coin flip, not a margin. It failed 4 runs
1265 # in 20 on an idle machine. At 250 it is 600 + 4*50 = 800ms, measured at
1266 # 798-799 across 12 runs. The same formula puts the SECOND keystroke at
1267 # 1000ms when DELAY_MS=400, which is the confirmed=1 collapse noted above.
1254 want_stat "$OUT.pb.err" made 5 "burst" 1268 want_stat "$OUT.pb.err" made 5 "burst"
1255 # `confirmed` is a LIVENESS counter and the only timing-dependent one here: 1269 # `confirmed` is a LIVENESS counter and the only timing-dependent one here:
1256 # the fifth confirmation can still be in flight when the detach byte lands, 1270 # the fifth confirmation can still be in flight when the detach byte lands,
@@ -1263,8 +1277,13 @@ want_stat "$OUT.pb.err" made 5 "burst"
1263 # a prediction judged wrong, or one that aged out unjudged, is a defect 1277 # a prediction judged wrong, or one that aged out unjudged, is a defect
1264 # whenever it happens, and nothing about timing makes either acceptable. 1278 # whenever it happens, and nothing about timing makes either acceptable.
1265 want_stat_ge "$OUT.pb.err" confirmed 4 "burst" 1279 want_stat_ge "$OUT.pb.err" confirmed 4 "burst"
1266 want_stat "$OUT.pb.err" contradicted 0 "burst" 1280 # `expired` first: an expiry runs through the same abandonAll tail as a
1281 # refutation and increments BOTH counters, so contradicted >= expired always.
1282 # Asserting contradicted first reports an age-out as "a prediction judged
1283 # wrong" — a different defect with a different cause, and the reason this
1284 # flake was first investigated as a reconcile bug.
1267 want_stat "$OUT.pb.err" expired 0 "burst" 1285 want_stat "$OUT.pb.err" expired 0 "burst"
1286 want_stat "$OUT.pb.err" contradicted 0 "burst"
1268 "$MUXD" dump --sock "$SOCK5" | grep -q "burst" || { 1287 "$MUXD" dump --sock "$SOCK5" | grep -q "burst" || {
1269 echo "e2e FAIL: burst: daemon grid does not hold the typed text"; exit 1; 1288 echo "e2e FAIL: burst: daemon grid does not hold the typed text"; exit 1;
1270 } 1289 }