e09da54f
fix: review conditions — soak refuses a dirty baseline; the scroll bug's true severity
a73x 2026-08-09 21:47
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -1620,8 +1620,11 @@ mode — it drops pending and retired predictions and nothing else. A | |||
| 1620 | reconnect taken while scrolled therefore leaves the client painting live | 1620 | reconnect taken while scrolled therefore leaves the client painting live |
| 1621 | rows with the overlay still suppressing, and the "any other key leaves | 1621 | rows with the overlay still suppressing, and the "any other key leaves |
| 1622 | scroll mode" branch can never rescue it because that branch is guarded by | 1622 | scroll mode" branch can never rescue it because that branch is guarded by |
| 1623 | `scroll_pages > 0`, which the reconnect has just made false. Local echo | 1623 | `scroll_pages > 0`, which the reconnect has just made false. Shift+PageDown |
| 1624 | prediction is off for the rest of the session, silently. Fixed | 1624 | does still clear it — its `scroll_pages == 0` arm calls |
| 1625 | `setScrollMode(false)` unconditionally — so the state is recoverable, by a | ||
| 1626 | keystroke no user has any reason to try. Local echo prediction is silently | ||
| 1627 | off until they do. Fixed | ||
| 1625 | client-side with one call next to the existing `scroll_pages = 0` — | 1628 | client-side with one call next to the existing `scroll_pages = 0` — |
| 1626 | deliberately not by making `flush()` clear the mode, because every | 1629 | deliberately not by making `flush()` clear the mode, because every |
| 1627 | snapshot flushes and a snapshot is not a reason to leave history; the | 1630 | snapshot flushes and a snapshot is not a reason to leave history; the |
| @@ -1671,8 +1674,9 @@ forward for another milestone. | |||
| 1671 | pen it could protect against belongs to the HOST terminal — which no | 1674 | pen it could protect against belongs to the HOST terminal — which no |
| 1672 | rendered-grid comparison can ever hold, since the render helper replays | 1675 | rendered-grid comparison can ever hold, since the render helper replays |
| 1673 | into a fresh engine at the default pen by construction. The unit test | 1676 | into a fresh engine at the default pen by construction. The unit test |
| 1674 | at `src/client.zig:1837` is the real guard and fails under the mutation | 1677 | `paintDeltaClipped skips rows beyond the tty and clamps the cursor` |
| 1675 | today; the bank names the dependency guarantee it rests on. | 1678 | (client.zig) is the real guard and fails under the mutation today; the |
| 1679 | bank names the dependency guarantee it rests on. | ||
| 1676 | - **Catch parity is not diagnosis parity** (rows 11 and 13). Both suites | 1680 | - **Catch parity is not diagnosis parity** (rows 11 and 13). Both suites |
| 1677 | catch both mutations, so the disposition column reads "both caught" and | 1681 | catch both mutations, so the disposition column reads "both caught" and |
| 1678 | says nothing interesting. What the columns hide: the old suite fails | 1682 | says nothing interesting. What the columns hide: the old suite fails |
docs/roadmap.md
| Old | New | ||
|---|---|---|---|
| @@ -65,8 +65,8 @@ markers; new suite **13 of 18**; the six-row delta is all paint, overlay | |||
| 65 | and clip — the byte-blind class, a wrong picture drawn from right bytes | 65 | and clip — the byte-blind class, a wrong picture drawn from right bytes |
| 66 | (decisions.md, M11, which carries the table in full). Zero rows ended | 66 | (decisions.md, M11, which carries the table in full). Zero rows ended |
| 67 | undecided. En route the campaign found a real bug in unmutated code — a | 67 | undecided. En route the campaign found a real bug in unmutated code — a |
| 68 | reconnect taken while scrolled left prediction silently off for the rest | 68 | reconnect taken while scrolled left prediction silently off until the |
| 69 | of the session — and a computed timing knife-edge in the burst scenario | 69 | user happened to press Shift+PageDown — and a computed timing knife-edge in the burst scenario |
| 70 | that had been sitting on the expiry bound exactly, whose formula also | 70 | that had been sitting on the expiry bound exactly, whose formula also |
| 71 | explains the M9-era `DELAY_MS=400` collapse retroactively. The rule | 71 | explains the M9-era `DELAY_MS=400` collapse retroactively. The rule |
| 72 | extracted: **a timing margin is a number you compute, not a sentence you | 72 | extracted: **a timing margin is a number you compute, not a sentence you |
src/client.zig
| Old | New | ||
|---|---|---|---|
| @@ -610,10 +610,14 @@ fn session( | |||
| 610 | // The overlay has to be told, and only the client can tell it: | 610 | // The overlay has to be told, and only the client can tell it: |
| 611 | // `flush()` drops predictions but deliberately leaves the mode | 611 | // `flush()` drops predictions but deliberately leaves the mode |
| 612 | // bit alone, so a reconnect taken while scrolled would leave the | 612 | // bit alone, so a reconnect taken while scrolled would leave the |
| 613 | // overlay suppressing forever. The exit-scroll branch below | 613 | // overlay suppressing with no page to suppress for. The |
| 614 | // cannot rescue it either — that one is guarded by | 614 | // "any other key" exit at the bottom of the loop cannot rescue |
| 615 | // `scroll_pages > 0`, which the line above has just made false. | 615 | // it — that branch is guarded by `scroll_pages > 0`, which the |
| 616 | // Prediction would be silently off for the rest of the session. | 616 | // line above has just made false. Shift+PageDown still can |
| 617 | // (its `scroll_pages == 0` arm clears the mode unconditionally), | ||
| 618 | // so this is recoverable rather than terminal — but only by a | ||
| 619 | // keystroke the user has no reason to guess, so prediction is | ||
| 620 | // silently off until they do. | ||
| 617 | overlay.setScrollMode(false); | 621 | overlay.setScrollMode(false); |
| 618 | if (!reconnect( | 622 | if (!reconnect( |
| 619 | alloc, | 623 | alloc, |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -167,8 +167,16 @@ assert_converged() { | |||
| 167 | CONV_COUNT=$((CONV_COUNT + 1)) | 167 | CONV_COUNT=$((CONV_COUNT + 1)) |
| 168 | converged_quiet "$1" "$2" || { | 168 | converged_quiet "$1" "$2" || { |
| 169 | echo "e2e FAIL: $3: client render diverges from daemon grid (-daemon +client):" | 169 | echo "e2e FAIL: $3: client render diverges from daemon grid (-daemon +client):" |
| 170 | head -40 "$1.diff" 2>/dev/null || \ | 170 | # An empty diff file is the styled-only case, not a passing one — |
| 171 | echo "(no plain diff: divergence is styled-only, or the helper failed)" | 171 | # the glyphs agree and the pens do not, which is precisely what the |
| 172 | # byte-exact leg exists to catch, so say so instead of printing | ||
| 173 | # forty lines of nothing. | ||
| 174 | if [ -s "$1.diff" ]; then | ||
| 175 | head -40 "$1.diff" | ||
| 176 | else | ||
| 177 | echo "(no plain diff: the grids agree on glyphs and differ on STYLE —" | ||
| 178 | echo " compare $1.dvt against $1.rvt)" | ||
| 179 | fi | ||
| 172 | echo "grids left in $1.render / $1.dump / $1.rvt / $1.dvt" | 180 | echo "grids left in $1.render / $1.dump / $1.rvt / $1.dvt" |
| 173 | exit 1 | 181 | exit 1 |
| 174 | } | 182 | } |
test/soak.sh
| Old | New | ||
|---|---|---|---|
| @@ -14,6 +14,19 @@ FAILDIR="$TMP/mux-soak-$$-failures" | |||
| 14 | SUMMARY="$TMP/mux-soak-$$.summary" | 14 | SUMMARY="$TMP/mux-soak-$$.summary" |
| 15 | LOG="$TMP/mux-soak-$$.log" | 15 | LOG="$TMP/mux-soak-$$.log" |
| 16 | : > "$SUMMARY" | 16 | : > "$SUMMARY" |
| 17 | # Residue predating the soak is not this soak's leak. Refuse to start rather | ||
| 18 | # than baseline-subtract: the files are probably evidence a previous failing | ||
| 19 | # e2e deliberately left behind, and the per-run hygiene check below cannot | ||
| 20 | # tell them apart from a leak — forcing a look is better than silently | ||
| 21 | # discarding them. | ||
| 22 | BASE_STRAYS=$(find "$TMP" -maxdepth 1 \( -name 'muxd-e2e-*' -o -name 'mux-e2e-*' \) 2>/dev/null | wc -l) | ||
| 23 | [ "$BASE_STRAYS" -eq 0 ] || { | ||
| 24 | echo "soak FAIL: $BASE_STRAYS mux e2e temp files already in $TMP before run 1." | ||
| 25 | echo " They are probably evidence from an earlier failing e2e." | ||
| 26 | echo " Inspect them, then clear them — the per-run hygiene check" | ||
| 27 | echo " cannot tell them apart from a leak." | ||
| 28 | exit 1 | ||
| 29 | } | ||
| 17 | FAILED=0 | 30 | FAILED=0 |
| 18 | i=1 | 31 | i=1 |
| 19 | while [ "$i" -le "$N" ]; do | 32 | while [ "$i" -le "$N" ]; do |