Skip to content

fix(daemon): the receive tick transmits and never dropped its capture stream (#1319) - #1322

Merged
dc0sk merged 1 commit into
mainfrom
fix/1319-rx-tick-post-transmit-drop
Sep 9, 2026
Merged

fix(daemon): the receive tick transmits and never dropped its capture stream (#1319)#1322
dc0sk merged 1 commit into
mainfrom
fix/1319-rx-tick-post-transmit-drop

Conversation

@dc0sk

@dc0sk dc0sk commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Closes #1319.

What was wrong

#1007 established that the daemon must not hold its capture stream across a keyed transmit: nothing reads it, so on cpal the callback appends to an unbounded buffer and the next tick is handed one discontinuous blob of audio captured while this station was transmitting.

rx_stream is assigned at four sites, all in server::run — and none is on the rx tick arm. Yet that arm keys the transmitter:

  • the OTA ACK (server.rs:~991)
  • a CONACK or QSY reply out of process_received_bytes (:1043)
  • the periodic §97.119 station ID (:1183)

The station ID makes it unconditional

I filed this expecting it to affect OTA ACKs and QSY replies. Working out how to trigger one showed the periodic station ID transmits on that arm too — so every compliant station held its capture stream across a transmit on a fixed schedule, not just one mid-exchange.

Magnitude scales with airtime: an ACK is short, a QSY line at BPSK31 is tens of seconds.

The fix

Snapshot frames_transmitted() at the top of the arm, drop on change at the bottom — the same shape as the command arm's :865 drop, and keyed on the counter rather than the emission for the reason that comment already gives: a future keyed emission on this arm must not silently miss it.

The fixture took three attempts, and the first two passed against unfixed code

§97.119 only obliges a station that has transmitted, so id_due requires tx_since_id. A daemon that has sent nothing never IDs, and both early versions were therefore vacuous.

The working shape uses one command that produces two keyed transmits on two different arms: the SendMessage on the command arm — whose drop is already correct — and the station ID it arms, which fires on the tick arm at once because last_id_ms starts at zero. The reopen count then discriminates directly.

Driven through a real daemon over TCP on the counting backend #1007's own test built.

Sabotage-verified: disabling the tick-arm drop yields 1 reopen against the required 2, while the file's two pre-existing tests stay green — so the failure is attributable to this arm, not to the command arm's.

GATE: PASS c3a96970778ac953057ea306b7628ccc38e6d6ad clean — 328 suites, 2516 passed, 0 failed.

This is a patch, not an answer

A third drop site in server::run treats the symptom of an ownership question. #1308 — who owns the capture stream — is the answer, and it has now been decided (the repeater consumes the daemon's burst stream). This closes a hole that would otherwise stay open for as long as that work takes, and it remains correct afterwards.

Consumer

server::run's rx tick arm (server.rs:868-1252), the only select! arm that both transmits and holds rx_stream.

Prior art

server.rs:826 (pre-OTA, variant-keyed) and :865 (post-command, counter-keyed) are the two existing drops; this is :865 applied to its sibling arm. #1007 (8711a06c) is the rule and the source of the counting-backend instrument.

Twins

rx_stream now has a drop on both arms that transmit. The watchdog arm does not transmit. Found by running the twins grep I had left UNCHECKED on #1312 — which is also where the QSY scan's own capture defect came from.

Refactors: CAP-55

🤖 Generated with Claude Code

https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6

… stream (#1319)

#1007 established that the daemon must not hold its capture stream across a keyed
transmit: nothing reads it, so on cpal the callback appends to an unbounded
buffer and the next tick is handed one discontinuous blob of audio captured while
this station was transmitting.

`rx_stream` is assigned at four sites and none is on the rx tick arm — yet that
arm keys the OTA ACK, a CONACK or QSY reply out of `process_received_bytes`, and
the periodic §97.119 station ID. That last one makes it unconditional: every
compliant station identifies on a schedule, so every station did this, not just
one mid-QSY-exchange. A QSY line at BPSK31 is tens of seconds of it.

Fixed the same way the command arm already is: snapshot `frames_transmitted()`
at the top of the arm, drop on change at the bottom. Keyed on the counter rather
than the emission, for the reason the existing comment gives — a future keyed
emission on this arm must not silently miss it.

Gated through a real daemon on the counting backend #1007's own test built. The
fixture took three attempts and the first two passed against unfixed code:
§97.119 only obliges a station that has transmitted, so the ID timer arms from TX
activity and a daemon that has sent nothing never IDs. The working shape uses one
command that produces two keyed transmits on two different arms, so the reopen
count discriminates — 2 with the fix, 1 without.

This is a patch on an ownership question, not an answer to it. #1308 — who owns
the capture stream — remains the answer; this closes a hole that would otherwise
stay open for as long as that decision takes.

Refactors: CAP-55

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
@dc0sk
dc0sk merged commit 98c53c3 into main Sep 9, 2026
6 of 7 checks passed
@dc0sk
dc0sk deleted the fix/1319-rx-tick-post-transmit-drop branch September 9, 2026 06:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The rx tick arm transmits and never drops rx_stream — #1007's post-transmit rule is enforced on the command arm only

1 participant