Skip to content

fix(daemon): a repeater that is not running is no longer reported as running (#1298) - #1309

Merged
dc0sk merged 1 commit into
mainfrom
fix/1298-repeater-thread-reporting
Sep 8, 2026
Merged

fix(daemon): a repeater that is not running is no longer reported as running (#1298)#1309
dc0sk merged 1 commit into
mainfrom
fix/1298-repeater-thread-reporting

Conversation

@dc0sk

@dc0sk dc0sk commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes #1298. Lands before #1297 deliberately — see "Ordering".

What was wrong

The EnableRepeater thread take()s the CrossBandRepeater out of the runtime state, so when that thread exits the repeater is gone. Nothing observed that:

  • repeater_enabled stayed true, so EnableRepeater answered "repeater already enabled" forever;
  • DisableRepeaterEnableRepeater hit the no-repeater arm, which logged a warn! and then set repeater_enabled = true and emitted RepeaterChanged { enabled: true } anyway.

So a daemon with no repeater reported a running one, and no command sequence could get back to a truthful state.

What changed

  • Enabling with nothing to run fails, with a CommandError naming why, instead of claiming success.
  • A thread that has exited is reaped at the top of both repeater commands, clearing repeater_enabled and emitting the false edge. At the top of the commands rather than from a periodic tick: the thread's own events notify clients promptly, and the reap only has to make the next command truthful — a tick poll would need runtime_state on the tick path for no added signal.
  • The thread reports its own unexpected exitCommandError plus a false edge, on the error path only. A clean stop is already reported by DisableRepeater, and emitting there too would put two false edges on one transition.

An existing test was pinning the defect

apply_repeater_enable_disable_emits_state_changes ran with RuntimeControlState::default() — which has repeater: None — and asserted that enabling succeeded. So "the daemon reports a running repeater when none exists" was a passing acceptance test, not an oversight. It now builds a real CrossBandRepeater so the success path is the one it tests.

The issue's own premise was stale, and the correction is mine

#1298 said the loop "exits on the first capture that does not decode, which on hardware is the first capture", making enabled-forever the normal state on a rig. #1300 changed that this morning: a non-decoding capture is now Ok(None) and the loop continues. What kills the thread now is a PTT fault or a transmit error.

So the normal state on a rig is not a dead thread reported as enabled — it is a live thread that never relays and never says so above DEBUG. Both defects survive the correction; the severity framing did not. The issue has been amended rather than left asserting something untrue.

Ordering

This is why it lands before #1297. Without it, "the repeater relays nothing because the band is quiet" and "the repeater thread died" are the same observation from outside the daemon — which would make an on-air attempt of #1297 undiagnosable.

Tests

  • enabling_a_repeater_that_does_not_exist_fails_instead_of_claiming_success
  • a_repeater_thread_that_exited_is_reaped_rather_than_reported_enabled — uses a disabled repeater, whose run_full_duplex returns Ok(0) at once, standing in for any exit the daemon did not ask for.

Sabotage-verified both ways: removing the reap calls fails the reap gate with "the dead thread was never reported"; reinstating the claim-success arm fails the enable gate with "the daemon reports a repeater as enabled while none exists".

GATE: PASS 67f6d2506ee14f83794a6db43a35754a578b37ea clean — 326 suites, 2502 passed, 0 failed.

Consumer

apply_command_to_engine (lib.rs:2130) on the daemon's control-command task; EnableRepeater / DisableRepeater are its only callers of this state.

Prior art

#1260 put the repeater on SharedPtt; #1300's Ok(None) change is what made this issue's premise stale. The CommandError-instead-of-silent-success shape follows #1285's rule that an unusable backend must not look like a working one.

Twins

The other spawned-thread state in the daemon is the PTT watchdog, which holds no ownership and cannot strand state this way. repeater_stop is cleared alongside repeater_thread so a reaped session cannot leave a live stop flag behind.

Filed alongside

#1308 — the repeater has no audio device configuration at all: both engines use the OS default input, and no config field anywhere could name a second card. #1297's accumulation fix cannot be exercised on a real two-rig station without it.

Implements: REQ-OBS-02

🤖 Generated with Claude Code

https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6

…running (#1298)

The `EnableRepeater` thread `take()`s the `CrossBandRepeater` out of the runtime
state, so when that thread exits the repeater is gone. Nothing observed it:
`repeater_enabled` stayed true, `EnableRepeater` answered "already enabled"
forever, and no command sequence could get back to a truthful state.

The no-repeater arm was worse: it logged a warning, then set
`repeater_enabled = true` and emitted `RepeaterChanged { enabled: true }`
regardless — so a daemon with nothing to run reported a running repeater.

Now: enabling with nothing to run returns a `CommandError` naming why; a thread
that has exited is reaped at the top of the repeater commands; and the thread
reports its own unexpected exit, on the error path only, since a clean stop is
already reported by `DisableRepeater`.

`apply_repeater_enable_disable_emits_state_changes` was pinning the defect — it
ran with `repeater: None` and asserted that enabling SUCCEEDED. It now builds a
real repeater so the success path is the one it tests.

The issue's own premise was corrected in the process: it claimed the loop exits
on the first non-decoding capture, making enabled-forever the normal state on a
rig. #1300 changed that. The normal state on a rig is a live thread that never
relays and never says so above DEBUG — which is why this lands before #1297,
where "no relay" and "thread dead" would otherwise be the same observation.

Implements: REQ-OBS-02

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
@dc0sk
dc0sk merged commit 185ce8d into main Sep 8, 2026
5 of 6 checks passed
@dc0sk
dc0sk deleted the fix/1298-repeater-thread-reporting branch September 8, 2026 14:56
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.

A dead repeater thread is reported as enabled forever, and re-enabling it emits RepeaterChanged{enabled:true} with no repeater

1 participant