Skip to content

Commit fcfd02e

Browse files
secupclaude
andcommitted
fix(arq): revert keepalive default-on — re-emits re-assert rung command (pins R1/4); file BUG-MPG20-OVER-DEMOTE-R14 (pre-existing Good@20->R1/4 over-demote, the real 'doesn't make sense')
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019btwFxDt7D19SZSqPYvZnk
1 parent 3935e79 commit fcfd02e

3 files changed

Lines changed: 63 additions & 2 deletions

File tree

docs/CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,35 @@ Verified: ctest 85/85; good@20 PASS 1600 bps.
8484

8585
---
8686

87+
## 2026-07-14 — fix(arq): keepalive ACK REVERTED to default-off — re-emits re-assert the rung command (pins R1/4 on a stall); + surfaced a pre-existing MPG@20 over-demote
88+
89+
Rig F293-F298 (MPG@20, keepalive default-on) exposed two problems (operator-
90+
caught: "R1/4 doesn't make sense" + "ACK while receiving"):
91+
92+
1. **KEEPALIVE FLAW (mine):** the keepalive re-emits the FULL tone-burst ACK,
93+
which carries the current rung command (rate_hint). During a stall the
94+
receiver's reading is momentarily low, so the re-emits (7× the same
95+
group_seq) RE-ASSERT rate_hint=1 → PIN R1/4 through any recovery, and the
96+
18 fires/transfer add ACK-vs-burst collision traffic (visible on the
97+
waterfall). A keepalive must trigger a RESEND, not re-command the rung.
98+
REVERTED to default-off. BLOCKER for default-on: emit a rung-NEUTRAL/HOLD
99+
keepalive (base+bitmap only, don't refresh rate_hint).
100+
101+
2. **PRE-EXISTING OVER-DEMOTE (the real "R1/4 doesn't make sense"):** at MPG@20
102+
— Multipath GOOD — the receiver read fading index 0.65 as **Moderate** and
103+
usable **8.8 dB** (channel 12.0, dial 20 → an 11 dB gap), so its authority
104+
demoted to R1/4 (38× rate_hint=1). The Good/Moderate discriminator
105+
fuzzy-boundary (0.65 straddles the threshold) + the large dial→usable gap
106+
over-demote a healthy Good@20 channel. FILED: this is a rate-selection
107+
accuracy issue independent of the keepalive — likely a bigger lever than the
108+
stall fix (over-demoting to R1/4 at Good@20 caps throughput hard).
109+
110+
The keepalive mechanism (cap the 44 s stall) remains sound + rig-proven
111+
(F290/F291 8 s cap; F292 universal 16 s cap); it needs the neutral-rung
112+
variant before default-on. Stays opt-in (ULTRA_KEEPALIVE_ACK=1).
113+
114+
---
115+
87116
## 2026-07-14 — feat(arq): keepalive ACK now DEFAULT-ON — universal channel-busy gate makes it collision-safe on GUI AND TNC
88117

89118
The TNC-path blocker is fixed. The keepalive's collision-safety no longer

docs/KNOWN_BUGS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,29 @@ Fixed/obsolete historical deep dives belong in `docs/CHANGELOG.md`.
2727
(~1.2 Hz clock/jitter wander over the 144 ms training window explains it exactly; sim reads
2828
10.1 dead-on). The operator display should eventually show BOTH numbers.
2929

30+
### BUG-MPG20-OVER-DEMOTE-R14: Good@20 read as Moderate + low usable → authority over-demotes to R1/4 (operator: "R1/4 doesn't make sense")
31+
32+
- **Rig F293-F298 (MPG@20, 2026-07-14).** At MPG@20 (Multipath GOOD, effective
33+
~15 dB) the receiver read fading index **0.65 → "Moderate"** and usable
34+
**8.8 dB** (physical channel 12.0; dial 20 → usable 8.8 = ~11 dB gap), so the
35+
RX authority commanded rate_hint=1 → **QPSK R1/4** (38× rate_hint=1 in one
36+
transfer). R1/4 at Good@20 caps throughput hard (2 bits × R1/4 vs the R2/3
37+
the channel supports).
38+
- **Two contributing causes (both pre-existing):** (a) the Good/Moderate
39+
Doppler discriminator fuzzy boundary — 0.65 straddles the Good/Moderate
40+
threshold, so Good@20 intermittently reads Moderate (see
41+
[[project_disc_radio_agnostic_fuzzy_boundary_2026_06_20]]); (b) usable 8.8 vs
42+
channel 12.0 vs dial 20 — the usable (effective/demod) SNR is ~3 dB under
43+
physical AND physical is ~8 dB under dial (fading Jensen + implementation
44+
loss), and the rate anchors may be too conservative for the honest-meter
45+
scale at this fading class.
46+
- **Likely a BIGGER throughput lever than the stall fix** — over-demoting a
47+
healthy Good@20 to R1/4 is a large, systematic loss. Investigate: is the
48+
fading misclassification the dominant cause (fix the discriminator boundary),
49+
or the usable-reading conservatism (fading class × anchor calibration)?
50+
- Independent of the keepalive (which merely AMPLIFIED it by re-asserting the
51+
low rung — see CHANGELOG 2026-07-14).
52+
3053
### BUG-ANCHOR-WAIT-NO-ACK-STALL: marginal-SNR bursts rejected in full-anchor-wait emit NO ACK → sender 44 s RTO stall (THE marginal-SNR throughput lever, quantified ~+30%)
3154

3255
- **Discovered/quantified 2026-07-14 (operator-caught "we didn't even ACK at

src/protocol/connection.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4432,10 +4432,19 @@ void Connection::tick(uint32_t elapsed_ms) {
44324432
// so it no longer depends on the GUI-only listen-before-ACK (which
44334433
// had blocked default-on: the headless TNC lacked it). Both the GUI
44344434
// (app.cpp) and the TNC (ultra_tnc.cpp) wire the query from the modem.
4435+
// DEFAULT-OFF (reverted 2026-07-14, rig F293-F298 MPG@20): the
4436+
// keepalive re-emits the FULL tone-burst ACK, which carries the
4437+
// current rung command (rate_hint). During a stall the receiver's
4438+
// reading is momentarily low (usable 8.8, fading misread Moderate),
4439+
// so the re-emits RE-ASSERT the low rung (rate_hint=1 → R1/4) 7×,
4440+
// PINNING R1/4 through recovery + adding ACK-vs-burst collision
4441+
// traffic (18 fires/transfer). A keepalive must trigger a RESEND,
4442+
// NOT re-command the rung. BLOCKER for default-on: emit a
4443+
// rung-NEUTRAL/HOLD keepalive (base+bitmap only, no rate_hint
4444+
// update). Until then opt-in.
44354445
static const bool keepalive_ack_enabled = [] {
44364446
const char* e = std::getenv("ULTRA_KEEPALIVE_ACK");
4437-
if (!e || e[0] == '\0') return true; // default-ON
4438-
return !(e[0] == '0' && e[1] == '\0'); // "0" opts out
4447+
return e != nullptr && e[0] != '\0' && e[0] != '0';
44394448
}();
44404449
// Threshold env-tunable (ULTRA_KEEPALIVE_ACK_MS, default 25000).
44414450
// The keepalive routes through listen-before-ACK (defers on channel

0 commit comments

Comments
 (0)