Skip to content

Commit fc75b5b

Browse files
secupclaude
andcommitted
feat(arq): keepalive ACK — break the marginal-SNR anchor-wait no-ACK stall (ULTRA_KEEPALIVE_ACK, default off)
BUG-ANCHOR-WAIT-NO-ACK-STALL: at marginal SNR, burst heads (corr 0.15-0.32) fall below the sync weak_accept floor (~0.45) → rejected in full-anchor-wait → NO ACK emitted → sender waits the full 44.68 s RTO. MPG@25 batch measured this as the top throughput drag (~+30% prize). Fix: the receiver re-emits its current cumulative ACK when its ACK side has gone silent > 25 s during an active file receive (rx_base_seq_>0 && last_rx_more_data_). The sender resends holes on ANY tone-burst ACK (turn boundary), so a re-ACK converts the 44 s RTO into a fast turnaround (~19 s saved/stall). No new protocol; routes through listen-before-ACK gating. Threshold 25 s > max burst airtime (window-16 ≈ 21.5 s) so it only fires on a genuine stall, not mid-legit-burst — no channel-busy plumbing needed for v1. v2 (filed): channel-gated shorter threshold for a bigger saving. Verified: ctest 86/86 (default-off byte-identical); sim good@20 PASS 2770. Rig A/B at MPG@25 next (the stall is a marginal-SNR phenomenon the sim can't reproduce). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019btwFxDt7D19SZSqPYvZnk
1 parent 9a97dbd commit fc75b5b

3 files changed

Lines changed: 46 additions & 0 deletions

File tree

src/protocol/connection.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4413,6 +4413,25 @@ void Connection::tick(uint32_t elapsed_ms) {
44134413
}
44144414

44154415
arq_.tick(elapsed_ms);
4416+
4417+
// KEEPALIVE ACK (BUG-ANCHOR-WAIT-NO-ACK-STALL, ULTRA_KEEPALIVE_ACK,
4418+
// default off): break the 44 s RTO stall when marginal bursts get
4419+
// rejected at sync (no ACK emitted) during an active file receive.
4420+
// Threshold 25 s > max burst airtime (window-16 ≈ 21.5 s) so it
4421+
// only fires on a genuine stall (no burst completed in the window),
4422+
// not mid-legit-burst — no channel-busy gating needed for v1. Fires
4423+
// before the 44.68 s RTO, saving ~19 s/stall. The re-emitted ACK
4424+
// routes through listen-before-ACK; the sender resends holes on any
4425+
// tone-burst ACK (turn boundary). v2 (filed): channel-gated shorter
4426+
// threshold for a bigger saving.
4427+
static const bool keepalive_ack_enabled = [] {
4428+
const char* e = std::getenv("ULTRA_KEEPALIVE_ACK");
4429+
return e != nullptr && e[0] != '\0' && e[0] != '0';
4430+
}();
4431+
if (keepalive_ack_enabled) {
4432+
arq_.keepaliveAckIfStalled(25000u);
4433+
}
4434+
44164435
// RX-AUTHORITY: age the verdict-SNR ring (stale readings must not
44174436
// steer the rung after a quiet stretch).
44184437
for (size_t i = 0; i < kRxAuthObsRing; ++i) {

src/protocol/selective_repeat_arq.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,7 @@ void SelectiveRepeatARQ::handleNackFrame(const v2::ControlFrame& frame) {
13681368

13691369
void SelectiveRepeatARQ::tick(uint32_t elapsed_ms) {
13701370
arq_time_ms_ += elapsed_ms;
1371+
keepalive_silent_ms_ += elapsed_ms; // reset on each sendSack() emit
13711372

13721373
if (ack_dedup_timer_ms_ > 0) {
13731374
if (elapsed_ms >= ack_dedup_timer_ms_) {
@@ -2044,6 +2045,19 @@ size_t SelectiveRepeatARQ::bufferedRxFrameCount() const {
20442045
return count;
20452046
}
20462047

2048+
bool SelectiveRepeatARQ::keepaliveAckIfStalled(uint32_t threshold_ms) {
2049+
if (!(rx_base_seq_ > 0 && last_rx_more_data_ &&
2050+
keepalive_silent_ms_ >= threshold_ms)) {
2051+
return false;
2052+
}
2053+
LOG_MODEM(INFO,
2054+
"SR-ARQ: keepalive ACK — receiver silent %u ms during active RX "
2055+
"(base=%u, stall recovery)",
2056+
keepalive_silent_ms_, rx_base_seq_);
2057+
sendSack(); // re-emit current cumulative state; resets keepalive_silent_ms_
2058+
return true;
2059+
}
2060+
20472061
void SelectiveRepeatARQ::sendSack() {
20482062
// MOVE-EPOCH: TOTAL ack silence while unanchored — rx_base_seq_ is still an
20492063
// old-era number, so ANY cumulative claim built from it would fabricate
@@ -2056,6 +2070,8 @@ void SelectiveRepeatARQ::sendSack() {
20562070
return;
20572071
}
20582072

2073+
keepalive_silent_ms_ = 0; // we are emitting an ACK now → not silent
2074+
20592075
uint32_t bitmap = buildRXBitmap();
20602076
uint16_t base_seq = (rx_base_seq_ - 1) & 0xFFFF;
20612077
const bool sack_has_final = rx_final_delivered_since_sack_;

src/protocol/selective_repeat_arq.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ class SelectiveRepeatARQ : public IARQController {
192192
}
193193
size_t getWindowSize() const { return config_.window_size; }
194194
uint16_t getRxBaseSeq() const { return rx_base_seq_; }
195+
196+
// Keepalive ACK (BUG-ANCHOR-WAIT-NO-ACK-STALL, 2026-07-14): when we are
197+
// actively receiving a file (more frames expected) but our ACK side has
198+
// gone silent for threshold_ms — bursts arriving yet rejected at sync
199+
// (marginal corr) or failing before decode, so no SACK was emitted — we
200+
// re-emit the current cumulative ACK. The sender treats ANY tone-burst ACK
201+
// as a turn boundary and resends the holes immediately, converting a full
202+
// 44 s RTO stall into a fast turnaround. Routes through the normal
203+
// listen-before-ACK channel gating. Returns true if a keepalive was sent.
204+
bool keepaliveAckIfStalled(uint32_t threshold_ms);
195205
uint16_t getTxBaseSeq() const { return tx_base_seq_; }
196206
// HARQ provisional keys (2026-07-01): the receiver's mirror of the
197207
// sender's next-burst seq fill = ascending !received seqs in the rx
@@ -389,6 +399,7 @@ class SelectiveRepeatARQ : public IARQController {
389399
// RX state
390400
std::array<RXSlot, MAX_WINDOW> rx_window_;
391401
uint16_t rx_base_seq_ = 0; // Next expected sequence
402+
uint32_t keepalive_silent_ms_ = 0; // ms since last SACK emit (stall detect)
392403
bool last_rx_more_data_ = false;
393404
uint8_t last_rx_flags_ = 0;
394405
v2::FrameType last_rx_frame_type_ = v2::FrameType::DATA;

0 commit comments

Comments
 (0)