Skip to content

Commit ab959e8

Browse files
secupclaude
andcommitted
feat(arq): keepalive ACK v2 (8s default threshold) — rig-proven (stall 44->8s, no collision); default-off, TNC listen-before-ACK gap blocks default-on (filed)
Direct gap measurement F290/F291: max silence capped at exactly 8.0s vs OFF 36.7s, reproducible, cwfails normal. Default-on reverted — broke UltraTncSimAudio (TNC lacks the GUI's listen-before-ACK gating the keepalive relies on). Blocker: move channel-clear gating into connection/ARQ (universal). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019btwFxDt7D19SZSqPYvZnk
1 parent 64e9a2b commit ab959e8

2 files changed

Lines changed: 35 additions & 1 deletion

File tree

docs/CHANGELOG.md

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

8585
---
8686

87+
## 2026-07-14 — feat(arq): keepalive ACK v2 (8 s) — rig-PROVEN (stall 44→8 s, no collision), default-off blocked by TNC listen-before-ACK gap
88+
89+
Direct silence-gap measurement (epoch-independent, unlike the goodput A/B)
90+
validated the fix cleanly. Rig F290/F291 (ULTRA_KEEPALIVE_ACK_MS=8000): **max
91+
ACK-silence gap capped at exactly 8.0 s** (vs OFF 36.7 s, v1@25s 25.0 s),
92+
reproducible across both runs, 13 keepalives/transfer, and cwfails NORMAL
93+
(266/491 — the faster firing did NOT collide, because it routes through the
94+
GUI's listen-before-ACK deferral). Default threshold set to 8000 (from 25000).
95+
96+
**Default-on ATTEMPTED and REVERTED:** flipping ULTRA_KEEPALIVE_ACK default-on
97+
broke UltraTncSimAudio (timeout 264 s vs 58 s off). Root cause: the keepalive's
98+
collision-safety depends on listen-before-ACK, which lives in the GUI app layer
99+
(app.cpp) — the HEADLESS TNC (ultra_tnc, the Winlink/PAT path) has NO such
100+
gating, so the keepalive TX'd over inbound bursts → collision. So it stays
101+
DEFAULT-OFF (opt-in on the rig/GUI where the gating exists).
102+
103+
**BLOCKER for default-on (filed):** move the channel-clear gating from the GUI
104+
app layer into the connection/ARQ (universal) so the keepalive is collision-
105+
safe on TNC too — then flip default-on at 8 s. The mechanism + rig safety are
106+
already proven; only the TNC-path gating remains.
107+
108+
---
109+
87110
## 2026-07-14 — feat(arq): keepalive ACK v1 (default off) + honest rig A/B — mechanism works, benefit epoch-noise-dominated; v2 (fire sooner) filed
88111

89112
Built the receiver keepalive ACK for BUG-ANCHOR-WAIT-NO-ACK-STALL: re-emit the

src/protocol/connection.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4424,6 +4424,17 @@ void Connection::tick(uint32_t elapsed_ms) {
44244424
// routes through listen-before-ACK; the sender resends holes on any
44254425
// tone-burst ACK (turn boundary). v2 (filed): channel-gated shorter
44264426
// threshold for a bigger saving.
4427+
// ULTRA_KEEPALIVE_ACK, DEFAULT-OFF. On the rig/GUI the 8 s keepalive
4428+
// is mechanism-proven (F290/F291: max ACK-silence gap capped at
4429+
// exactly 8.0 s vs OFF's 36.7 s) and collision-free — because it
4430+
// routes through the GUI's listen-before-ACK (app.cpp), so a
4431+
// mid-burst fire DEFERS. BLOCKER for default-on: the HEADLESS TNC
4432+
// path (ultra_tnc) has NO listen-before-ACK gating, so default-on
4433+
// broke UltraTncSimAudio (keepalive TX'd over inbound bursts →
4434+
// collision → timeout). Fix before flipping: move the channel-clear
4435+
// gating into the connection/ARQ (universal) rather than the GUI
4436+
// app layer, so the keepalive is safe on TNC too. Until then:
4437+
// opt-in on the GUI/rig where the gating exists.
44274438
static const bool keepalive_ack_enabled = [] {
44284439
const char* e = std::getenv("ULTRA_KEEPALIVE_ACK");
44294440
return e != nullptr && e[0] != '\0' && e[0] != '0';
@@ -4440,7 +4451,7 @@ void Connection::tick(uint32_t elapsed_ms) {
44404451
const long v = std::atol(e);
44414452
if (v >= 3000 && v <= 60000) return static_cast<uint32_t>(v);
44424453
}
4443-
return 25000u;
4454+
return 8000u; // v2 (rig-proven): caps stall at 8 s, no collision
44444455
}();
44454456
if (keepalive_ack_enabled) {
44464457
arq_.keepaliveAckIfStalled(keepalive_ack_ms);

0 commit comments

Comments
 (0)