Skip to content

Commit 64e9a2b

Browse files
secupclaude
andcommitted
feat(arq): keepalive ACK threshold env-tunable (ULTRA_KEEPALIVE_ACK_MS) + honest v1 A/B result — mechanism works, epoch-noise-dominated, v2 (fire at ~8-10s) filed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019btwFxDt7D19SZSqPYvZnk
1 parent fc75b5b commit 64e9a2b

3 files changed

Lines changed: 50 additions & 2 deletions

File tree

docs/CHANGELOG.md

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

8585
---
8686

87+
## 2026-07-14 — feat(arq): keepalive ACK v1 (default off) + honest rig A/B — mechanism works, benefit epoch-noise-dominated; v2 (fire sooner) filed
88+
89+
Built the receiver keepalive ACK for BUG-ANCHOR-WAIT-NO-ACK-STALL: re-emit the
90+
current cumulative ACK when the ACK side has been silent > threshold (default
91+
25 s, env ULTRA_KEEPALIVE_ACK_MS) during an active receive (rx_base>0 &&
92+
last_rx_more_data). Sender resends holes on any tone-burst ACK → converts the
93+
44.68 s RTO stall into a faster turnaround. Env-gated ULTRA_KEEPALIVE_ACK,
94+
default off; ctest 86/86 byte-identical; routes through listen-before-ACK.
95+
96+
Rig A/B (F284-F289, interleaved MPG@25, KA off vs on): the keepalive FIRES
97+
correctly (1-2×/transfer, exactly on the anchor-reject stalls) and is safe
98+
(cannot cause failures). But the A/B is INCONCLUSIVE — the KA-ON arm drew
99+
consistently rougher epochs (fails 2386/487/860 vs OFF 386/216/645; means ON
100+
1.51 vs OFF 2.45 is pure epoch luck). The one clean pair (F288/F289, comparable
101+
fails) is a faint POSITIVE: ON matched OFF's goodput despite 33% more fails.
102+
103+
LESSON: the 25 s threshold catches the stall LATE (~19 s of 44 s saved). Since
104+
the keepalive routes through listen-before-ACK (mid-burst fire is DEFERRED not
105+
collided), a shorter threshold is SAFE. **v2 (do next): ULTRA_KEEPALIVE_ACK_MS
106+
≈ 8000-10000** — catches the stall ~15 s earlier (~36 s saved), no rebuild.
107+
Or sender-side "no-ACK-after-burst → immediate full-anchor resend". Keep v1
108+
default-off until v2 is rig-A/B-confirmed with a many-pair batch (the effect
109+
is intermittent + small vs the huge MPG@25 epoch noise).
110+
111+
---
112+
87113
## 2026-07-14 — diag: MPG@25 natural batch + BUG-ANCHOR-WAIT-NO-ACK-STALL (the marginal-SNR throughput lever, ~+30%)
88114

89115
MPG@25 natural (F279-F283, ACE off, no force): 1.80/2.48/1.38/2.32/2.81,

docs/KNOWN_BUGS.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,15 @@ Fixed/obsolete historical deep dives belong in `docs/CHANGELOG.md`.
4949
tone-burst ACK with the cumulative base UNCHANGED** ("no progress, resend") —
5050
which breaks the 44 s silence and gets an immediate resend. So the receiver
5151
does NOT need to decode the weak burst; it only needs to NOT be silent.
52-
- **Fix approaches (next session — protocol/sync path, rig-validate; ACK
52+
- **Status (2026-07-14): v1 SHIPPED (keepalive ACK, ULTRA_KEEPALIVE_ACK default
53+
off, fc75b5b+). Mechanism confirmed on rig (fires 1-2×/transfer on the
54+
stall), safe (ctest clean, cannot cause fails), but the 25 s threshold
55+
catches the stall late and the A/B was epoch-noise-dominated (inconclusive,
56+
faint positive in the one clean pair). NEXT = v2: ULTRA_KEEPALIVE_ACK_MS
57+
≈ 8-10 s (safe — routes through listen-before-ACK, mid-burst fire is
58+
deferred; catches the stall ~15 s earlier) + many-pair rig A/B; then flip
59+
default-on. See CHANGELOG 2026-07-14.**
60+
- **Fix approaches (protocol/sync path, rig-validate; ACK
5361
contract has regressed before, build carefully):**
5462
1. ACK-LIVENESS ACCEPT: when a burst is rejected in full-anchor-wait but
5563
light_found with clear energy (corr above a low floor, e.g. 0.15 — a real

src/protocol/connection.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4428,8 +4428,22 @@ void Connection::tick(uint32_t elapsed_ms) {
44284428
const char* e = std::getenv("ULTRA_KEEPALIVE_ACK");
44294429
return e != nullptr && e[0] != '\0' && e[0] != '0';
44304430
}();
4431+
// Threshold env-tunable (ULTRA_KEEPALIVE_ACK_MS, default 25000).
4432+
// The keepalive routes through listen-before-ACK (defers on channel
4433+
// busy), so a mid-legit-burst fire is deferred, not collided —
4434+
// meaning a SHORTER threshold (v2, ~8-10 s) is safe and catches the
4435+
// stall ~15 s earlier. Left at 25 s by default pending the v2 rig
4436+
// A/B (F284-F289 at 25 s: fires 1-2×/transfer on the real stall,
4437+
// benefit epoch-noise-dominated).
4438+
static const uint32_t keepalive_ack_ms = [] {
4439+
if (const char* e = std::getenv("ULTRA_KEEPALIVE_ACK_MS")) {
4440+
const long v = std::atol(e);
4441+
if (v >= 3000 && v <= 60000) return static_cast<uint32_t>(v);
4442+
}
4443+
return 25000u;
4444+
}();
44314445
if (keepalive_ack_enabled) {
4432-
arq_.keepaliveAckIfStalled(25000u);
4446+
arq_.keepaliveAckIfStalled(keepalive_ack_ms);
44334447
}
44344448

44354449
// RX-AUTHORITY: age the verdict-SNR ring (stale readings must not

0 commit comments

Comments
 (0)