Repository navigation
fix(figc4.4 + figc4.5 + §4.4.2/§6.4.4): discard out-of-window duplicate I frames — X.25 2.4.6.4(a) guard, bound = negotiated k (#40) - #81
Conversation
…(a) guard (#40) The Connected I-received out-of-sequence arm treated ANY N(s) != V(r) as a fresh out-of-sequence frame: a late duplicate of an already-acknowledged frame was saved into the receive buffer (SREJ) or answered with REJ, and with an empty sender queue the SREJ names a frame that will never exist — an exception nothing can clear, re-sent on every poll per 4.4.4 (observed on air against BPQ 6.0.24.79, 2026-09-09; fuzzer-provable SREJ ping-pong livelock; see #40). - DataLink_Connected.graphml: new decision "V(r) < N(s) < V(r) + k?" spliced between "N(s) == V(r)?":No and "Reject Exception?", so every out-of-sequence path (SREJ and REJ branches alike) passes the guard and it precedes "Save Contents of I Frame". The No arm merges into the existing reject-exception discard / "P == 1?" chain — discard, no exception, RR F=1 only to a poll; no new action boxes. The bound is the k this receiver granted, expressed symbolically — direwolf's GENEROUS_K 63 constant collapses at modulo 8. Out-of-sequence subtree shifted one column right to make room; no edge/node overlap regressions (checked mechanically against HEAD). - connected.sdl.yaml regenerated by the Packet.Sdl.Transcribe walker: 68 transitions / 39 decisions (was 66 / 38). New decision i_vr_lt_ns_lt_vr_plus_k, predicate vr_lt_ns_lt_vr_plus_k; six t26_i_received out-of-sequence ids re-suffixed, two new guard:No discard transitions. - connected.citations.yaml re-keyed for the renames and annotated: ERRATUM (#40) entries for the two new transitions (4.4.2, direwolf is_ns_in_window, linbpq CheckNSLoop negative evidence). - spec-sdl/predicates.yaml: vr_lt_ns_lt_vr_plus_k added to the sequence_variables catalog (new atom, no legacy binding — downstream runtimes must bind it). - connected.g.mmd + DataLink_Connected.svg regenerated. Adjudication and evidence: see PR body and #40. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTRUwL676xWw3WHu1mKPE
…s figc4.4 (#40) TimerRecovery carries the identical I-received out-of-sequence decision chain, and is where the on-air trigger actually fires: the peer's checkpoint retransmission (driven by a stale F=1 N(R)) delivers the already-acknowledged N(s) while this end is in Timer Recovery. - DataLink_TimerRecovery.graphml: same splice as figc4.4 — new "V(r) < N(s) < V(r) + k?" decision between "N(s) == V(r)?":No and "Reject Exception?"; its No arm merges into the existing discard / "P == 1?" chain. "Reject Exception?" moved one staircase step down-right to make room; the figure's diagonal idiom is preserved and no edge/node overlap regressions (checked mechanically against HEAD). - timer_recovery.sdl.yaml regenerated by the Packet.Sdl.Transcribe walker: 92 transitions / 47 decisions (was 90 / 46). Same i_vr_lt_ns_lt_vr_plus_k decision id and vr_lt_ns_lt_vr_plus_k predicate as figc4.4; six t22_i_received ids re-suffixed, two new guard:No discard transitions. - timer_recovery.citations.yaml re-keyed and annotated (ERRATUM (#40) entries referencing the figc4.4 rationale). - timer_recovery.g.mmd + DataLink_TimerRecovery.svg regenerated (DataLink_TimerRecovery.srej-received.svg viewport is untouched by this splice). Kept separate from the figc4.4 commit so the two figures can be reviewed and reverted independently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTRUwL676xWw3WHu1mKPE
… the figures now draw (#40) Section 4.4.2 disposed only of the information field ("otherwise, it is discarded") and never said whether a sequence-error exception is raised; 6.4.4.2 read literally ordered an SREJ for ANY N(S) != V(R), with no window qualifier, while requiring the SREJ to carry "the value N(R) of the missing frame" — for a duplicate behind V(R) no frame is missing and there is no coherent N(R) to send. The prose was internally inconsistent, and an executable SDL cannot leave "is an exception raised?" implicit. - 4.4.2: new paragraph — N(S) outside "greater-than V(r)" / "less-than V(r)+k" (k = the window granted by this receiver) is a duplicate of an already-acknowledged frame: discard, raise no REJ/SREJ exception, respond RR F=1 only to P=1, whether or not SREJ is negotiated. Points at the new figc4.4/figc4.5 decision. - 6.4.4.1: REJ conditioned on N(S) being within the receive window; out-of-window duplicates raise nothing (the guard covers the REJ branch too — trading a spurious go-back-N burst for a T1 wait). - 6.4.4.2: retention conditioned on the window; new paragraph for the out-of-window case. - "Changes from Version 2.2" table: row appended. Refs #40. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTRUwL676xWw3WHu1mKPE
SVG figure changes (
|
| what | change | |
|---|---|---|
| + | node | V(r) < N(s) < V(r) + k? |
| + | label | No (by Discard Contents of I Frame) |
| + | label | Yes (by V(r) < N(s) < V(r) + k?) |
| + | edge | N(s) == V(r)? → V(r) < N(s) < V(r) + k? |
| + | edge | V(r) < N(s) < V(r) + k? → Discard Contents of I Frame |
| + | edge | V(r) < N(s) < V(r) + k? → Reject Exception? |
| − | edge | N(s) == V(r)? → Reject Exception? |
spec-sdl/v2.2-errata/data-link/svg/DataLink_TimerRecovery.svg
nodes 240→241, edges 270→272, labels 112→114
| what | change | |
|---|---|---|
| + | node | V(r) < N(s) < V(r) + k? |
| + | label | No (by Discard Contents of I Frame) |
| + | label | Yes (by Reject Exception?) |
| + | edge | N(s) == V(r)? → V(r) < N(s) < V(r) + k? |
| + | edge | V(r) < N(s) < V(r) + k? → Discard Contents of I Frame |
| + | edge | V(r) < N(s) < V(r) + k? → Reject Exception? |
| − | edge | N(s) == V(r)? → Reject Exception? |
Interactive viewers — swipe, onion-skin, blink, and a clickable change list that zooms to each change — are attached to this run as the svg-figure-diff artifact: download, unzip, open the HTML.
Locally: python3 tools/svgdiff/svg_diff.py main HEAD --all --merge-base
A text was treated as a node's caption when it fell inside the node's bounding box less a small inset. On a diamond that box over-claims the corners by nearly half the node's width — exactly where branch labels sit — so a "Yes" beside a decision could be counted as that decision's caption. Because the caption forms part of the node signature, one label drifting across that boundary reported the node as removed and re-added, and dragged every edge naming it into the diff as well. Nodes now carry their outline, built from the shape the renderer drew: polygon points as-is, rectangles as their corners, ellipses and circles sampled. Ownership keeps the existing inset box as a cheap reject and then confirms against the outline, so the test is strictly narrowing — it can only reject a claim the box test used to make, never add one. A path-shaped node still falls back to its box, since the numbers in `d` cannot be paired into vertices without a real path parser. That leaves one known false pair on DataLink_TimerRecovery.srej-received.svg, where a glyph node drawn as a path claims a nearby "No". It predates this change. Measured on a geometry-only change — #86, which moves every edge endpoint but alters nothing structural, so every finding it produces is false: 28 findings before, 13 after. On the worst affected figure, 14 before and 4 after. The findings for #81 are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188JcqNg5JDQqdsBt7LrbyE
|
Claude Code review — 3 finding(s) normal: doc/ax.25.2.3-draft.md:9-1170Errata row and §4.4.2 body assert Figures C4.4 and C4.5 now contain a 'V(r) < N(s) < V(r) + k?' decision, but the embedded PNG figures are not updated in this PR — readers who follow the figure references find no such box. nit: spec-sdl/predicates.yaml:68-70X.25 sub-clause citation inconsistent with the rest of the change: predicates.yaml cites nit: spec-sdl/v2.2-errata/data-link/yaml/connected.citations.yaml:82-87Legacy-id → t26 mapping table in the file header (lines 82-87) not updated for the #40 rename: it still lists the six old keys (e.g. Generated by Claude Code |
…mapping table for the #40 re-suffix Two of the three review findings (the third — regenerating the embedded PNG figures in the prose docs — is deferred; PNG generation is being re-implemented separately): - spec-sdl/predicates.yaml: the vr_lt_ns_lt_vr_plus_k comment cited X.25 §2.4.6.4(b) where the rest of the change cites §2.4.6.4(a). Both are involved and the comment now says which is which: (b) is the window-membership test the diamond asks; (a) is the disposal of a frame that fails it (discard, respond only to P=1). - connected.citations.yaml: the 2026-06-01 legacy-id → t26 header mapping still pointed the six out-of-sequence rows at their pre-#40 ids, two of which are now re-minted for the new guard:No discard transitions. Rows updated to the current ids with a note. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTRUwL676xWw3WHu1mKPE
…nerate backends (#81) Mechanical pin-bump + regen PR, the follow-up packethacking/ax25spec#81 specified (same vehicle as #78 for the figc4.5 SREJ fix). - ax25spec submodule: abd46b0 → 344aa97 (packethacking/ax25spec#81 merged): figc4.4 + figc4.5 gain the X.25 2.4.6.4(a) out-of-window duplicate-discard guard — a new "V(r) < N(s) < V(r) + k?" decision on the I-received out-of-sequence arm, bound = the receiver's granted k. The intervening upstream commits are the svgdiff tooling (#82-#85) and doc/media only — no other data-link table changes. - Regenerated connected + timer_recovery tables across all 7 backends (C#/Go/TS/JSON/Rust/C/Python). Closed-set churn: Ax25Guard gains vr_lt_ns_lt_vr_plus_k (Ax25Guard.g.cs / ax25-guard.g.ts / ax25_guard.g.rs). NOTE: the new guard atom has NO runtime binding yet — packet.net Ax25SessionBindings.cs and ax25-ts session-bindings.ts must bind it before consuming these backends (the bindings lint silently skips when those files are absent). - TranscribeTests pins: Connected 66/38 → 68/39, TimerRecovery 90/46 → 92/47; both transcribe snapshot fixtures refreshed. Walker output: Connected 68 transitions / 39 decisions (was 66 / 38); TimerRecovery 92 / 47 (was 90 / 46); zero branch: Undefined, zero next: Undefined on both pages. Verified in the compiled JSON: the six out-of-sequence paths carry vr_lt_ns_lt_vr_plus_k=true ahead of the reject/SREJ decisions, and the two new guard-false paths discard with no exception (RR F=1 only when P=1). C# 270 conformance + 150 codegen tests green locally (gofmt / rustfmt 1.9.0-stable / clang-format 22.1.5 on PATH). Claude-Session: https://claude.ai/code/session_01EvTRUwL676xWw3WHu1mKPE Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #40. Figure + prose in one PR, following the #38/#65 (abd46b0) precedent: figc4.4 and figc4.5 as two independently revertable commits, prose in the v2.3 draft as a third.
What changed in each figure, and why
Both
I-received out-of-sequence arms treated any N(s) ≠ V(r) as a fresh out-of-sequence frame. A late duplicate of an already-acknowledged frame was saved into the receive buffer (SREJ branch — the misdelivery vector) or answered with REJ; with an empty sender queue the SREJ names a frame that will never exist, an exception nothing can clear, re-sent on every poll per §4.4.4 (the 3m22s on-air capture against BPQ in #40, plus the fuzzer ping-pong livelock).DataLink_Connected.graphml(commit 1): new decision diamond spliced betweenN(s) == V(r)?:No andReject Exception?. Its No arm merges into the existing reject-exception discard arm (Discard Contents of I Frame→P == 1?→ RR F=1 / nothing) — no new action boxes, exactly the packet.net formulation quoted in figc4.4/figc4.5 Connected I_received lacks the X.25 §2.4.6.4(a) out-of-window duplicate-discard guard #40. The out-of-sequence subtree (Reject Exception?through the own-busy discard arm) shifted one column right (+202.5) to make room; theReject Exception?:Yes edge re-routed down-then-left into the discard box's right side.DataLink_TimerRecovery.graphml(commit 2): identical splice;Reject Exception?moved one staircase step down-right (midway toSREJ Enabled), preserving the page's diagonal idiom. figc4.5 is where the on-air trigger actually fires — the peer's checkpoint retransmission (stale F=1 N(R)) is what delivers the already-acknowledged N(s).Guard placement nuance (one step earlier than the issue comment's "before the srej_enabled split"): the diamond sits before
Reject Exception?, not betweenReject Exception?:No andSREJ Enabled?. Semantics are identical — the reject-exception:Yes arm already discards, and the guard's No arm routes to that same discard box — but this way all six out-of-sequence transitions route through the guard, and the drawing needs no crossing edges. The guard precedesSave Contents of I Frameon every path, closing the misdelivery vector.The question text and what it mints
Drawn verbatim in both figures (matching the existing
V(a) <= N(r) <= V(s)?chained-comparison idiom):Walker-minted:
id: i_vr_lt_ns_lt_vr_plus_k,predicate: vr_lt_ns_lt_vr_plus_k(verified by running Packet.Sdl.Transcribe; identical in both pages). Registered inspec-sdl/predicates.yamlundersequence_variableswith a comment fixing the semantics: open interval per §4.4.2 / X.25 §2.4.6.4(b), modulo comparison, k = the window size this receiver granted — never a constant. direwolf'sGENEROUS_K 63collapses at modulo 8 (63 mod 8 = 7: the only value ever discarded is the frame exactly one behind V(r); two behind is saved as a future frame), and modulo 8 + SREJ is reachable (BPQ proposes exactly that; the #40 capture ran on it).Walker output (house format)
DataLink_Connected.graphml→ connected.sdl.yaml: 68 transitions / 39 decisions (was 66 / 38), zerobranch: Undefined, zeronext: Undefined(before and after).DataLink_TimerRecovery.graphml→ timer_recovery.sdl.yaml: 92 transitions / 47 decisions (was 90 / 46), zerobranch: Undefined, zeronext: Undefined(before and after).Per figure: the six out-of-sequence transitions gain
{ decision: i_vr_lt_ns_lt_vr_plus_k, branch: Yes }and re-suffix (insertyes_after..._no_no_); two new guard:No transitions per figure (..._no_no_no_yes= discard + RR F=1,..._no_no_no_no= discard only). Note the ids..._no_no_no_{yes,no}are re-minted — the pre-change..._no_no_no_nowas the REJ arm; the citations files carry a CAUTION comment about this.Citations: both
.citations.yamlfiles re-keyed for the renames, header note added in the #55-correction style, and ERRATUM (#40) entries written for the four new transitions (§4.4.2 quotes; direwolfis_ns_in_windowwith the GENEROUS_K caveat; linbpqSDIFRM/CheckNSLoopas negative evidence — linbpq line numbers were verified at560e9fcper #40, while the file's pinned commit is88a6898; function anchors are the primary reference).Prose amended (v2.3 draft only; v2.2.4 stays frozen, per the abd46b0 convention)
The three open editorial calls (from the #40 fix spec)
k <= modulus/2clamp → stays in Window wrap — k should always be less than half the modulus? #13. Not drawn, not put in prose. The predicates.yaml comment records that the bound is the receiver's granted k; the Window wrap — k should always be less than half the modulus? #13 companion invariant (window guard alone is porous at mod 8 when k ≥ 5) still needs its own erratum, and this PR does not pretend to cover it.Verification
Packet.Sdl.Transcribe(tooling pinff5bc7d; the Transcribe walker is byte-identical between the pin and ax25sdl HEAD4212c1e) regenerates exactly the committed*.sdl.yaml;tools/render/render_all.pyregenerates exactly the committed SVGs.Packet.Sdl.CodeGenruns clean through all pages and lints (the new predicate passes the catalog-completeness lint);dotnet test: conformance 270/270 pass; codegen tests show exactly the four expected movers —Walks_every_existing_state_pagepins (Connected 66/38 → 68/39, TimerRecovery 90/46 → 92/47) and the twoYaml_round_trips_…_matches_committed_snapshotfixtures — plus 8 RustEmitter failures that are pre-existing on this machine (rustfmt not installed; identical failure set with the submodule at the old pin)..g.mmdfiles committed here were produced by the ax25sdl CodeGen mermaid emitter against this branch.Follow-up work this implies (not in this PR)
.github/ax25sdl-tooling-refconsumers, updateTranscribeTests.cspins to (68, 39) and (92, 47), regenerate theFixtures/transcribe/*.sdl.yamlsnapshots and the seven language backends.vr_lt_ns_lt_vr_plus_khas no legacy binding. packet.netAx25SessionBindings.csand ax25-tssession-bindings.tsmust bind it (modulo comparison, receiver's granted k) before consuming the regenerated backends — the bindings lint silently skips when those files are absent from the codegen working dir, so this will not fail CI anywhere; it has to be tracked manually.Not verified / caveats
connected.citations.yaml, per the same caveat in figc4.4/figc4.5 Connected I_received lacks the X.25 §2.4.6.4(a) out-of-window duplicate-discard guard #40.SReject Exception > 0?:No entry intoN(s) > V(r) + 1?in figc4.4) exist in the committed renders and are deliberately left untouched so the SVG diffs stay scoped to the splice.🤖 Generated with Claude Code
https://claude.ai/code/session_01EvTRUwL676xWw3WHu1mKPE