aethersdr / aethersdr/AetherSDR
aetherd — multi-client TX arbiter and daemon transmit grants
@rfoust is already working on this.
Since Sep 19, 2026.
- Dominant language
- C++
- Stars
- 221
- Forks
- 117
- Avg merge
- 2d 9h
- Merged PRs (30d)
- 302
Description
Child of #3849 (aetherd RFC), which owns this architecture. Continues the
Stage 4 TxCoordinator work.
Why this issue exists
aetherd is observe-only unless --allow-local-control is passed, and daemon
transmit grants do not exist. TxCoordinator has the ownership and
cancellation machinery, but nothing above it can represent a client's transmit
authority, and its bounded-duration policy is — in the words of
docs/aetherd-stage4-tx-coordinator.md — "tested in isolation but not granted
to daemon clients by this increment."
This issue closes that gap: generic multi-client transmit authorization and
ownership for the daemon, with no dependency on any particular consumer.
In scope
- Multi-client TX arbiter above
TxCoordinator— per-client actor identity,
not the single transitional desktop compatibility actor - Daemon transmit grants over the versioned protocol, with the existing
admissionRefusalreasons made reachable for real clients - Wire up the bounded-duration policy that
docs/aetherd-stage4-tx-coordinator.mddescribes as "tested in isolation
but not granted to daemon clients by this increment" - Grant revocation and expiry paths, including the cancellation fence
behaviour already specified forTxCoordinator - Tests per the test-layer boundary table in
AGENTS.md— socket-free,
injected transport, covering refusals and non-events
Three prerequisites, added 2026-09-18 after @rfoust's review
1. Qualified stop and handoff must land in the same increment as bounded actors
This is not optional and not schedulable later —
TxCoordinator.h
says so in its own invariant comment: expire() is inert today only because
every production actor carries maximumOperationMs == 0, and the increment
that registers a bounded actor turns that comment into a live hazard. An
unacknowledged stop keeps recovering() true and refuses every later
acquire() for the rest of the session.
- Define qualifying stop evidence per enabled backend and activity, bound to
the correct connection and operation generation. A drained queue, a sent
command and a fixed delay are not radio-stop proof. - Unsupported backend/activity combinations stay unavailable, or require
qualified transport recovery. Transport teardown remains the present recovery
boundary. - Clearing recovery merely because software sent unkey risks handing ownership
to the next client while the prior radio-buffered tail or terminal writer is
still active. - State the boundary explicitly: this arbiter is not proof of exclusive
hardware ownership. Physical PTT or another independent radio client can key
the radio without passing through it.
2. Operation duration and grant expiration are separate contracts
ActorPolicy
has mayTransmit and maximumOperationMs — a per-transmission timeout, not
an absolute arming deadline. Once a completed operation is acknowledged, a
fresh operation starts a fresh window, and expire() only examines active or
unconfirmed operations, never idle authorization lifetime. Four distinct
contracts, to be named separately:
- Maximum continuous operation duration.
- Absolute grant lifetime (the parent's bounded arming period).
- Explicit operator-presence acknowledgment (parent D13).
- Transport keepalive.
Define renewal authority and monotonic engine scheduling that fires even when
no request arrives. Reacquisition, reconnect and automatic retry must not
silently renew operator authorization, and a restart must never restore an armed
grant.
3. Trusted grant-to-client binding, with one arbiter preserved
ControlSession
deliberately cannot represent TX today. Define who issues and revokes a grant,
and how it binds authenticated principal, control connection, radio/connection
generation, actor, producers and permitted operations.
Client names, PTT-source labels, producer IDs and --allow-local-control are
not transmit authority. Trust and identity live in the layer above
TxCoordinator; the coordinator stays the single engine admission and ownership
authority. Also required: versioned operations, stable refusal mappings,
observable ownership and recovery state, bounded registration limits, and
disconnect cleanup. A busy request must never become a delayed surprise
transmission. Preserve the desktop compatibility contract until independent-
client migration is deliberate; ordinary client cleanup stays owner-scoped, with
operator emergency stop a distinct authority.
Explicitly out of scope
No SIP, no RSB, no bearer work, no new dependency. Nothing here should reference
a call, a SIP session or dialog, or a codec. (Generic ControlSession and
RadioSession necessarily participate — the earlier bare "session" here was
wrong.)
No consumer-specific arbiter, ever. TxCoordinator remains the single
engine admission and ownership authority, and any future feature that wants to
transmit through the daemon uses this grant mechanism rather than growing its
own.
Depends on
Nothing. This is shared aetherd architecture under #3849, reviewed on its own
merits.
History
This issue was originally filed as Phase 1 of the SIP SBC milestone (RFC #5688,
decision D11), because that RFC's D4 ruling forced the arbiter to exist before
any SIP work could transmit. RFC #5688 is now paused and its other phases are
closed — but this work was never SIP-specific, and @rfoust's review
(#5690 comment,
and on #5688) recommended it stay generic and shared. It is therefore re-homed
under #3849 and survives the pause, unchanged in substance.
Evidence to close
- Handoff: A transmits, A stops, B stays refused until qualified stop
evidence arrives, then B is admitted only on fresh intent. - Lifecycle matrix: normal completion, cancel, revoke, expiry,
disconnect/reconnect, delayed / duplicate / stale acknowledgment, and an
in-flight terminal write. - Grant deadlines: expiry while idle and while transmitting; repeated
completed operations crossing the absolute grant deadline; timer/request
races. Queued and captured media stays cancelled. - Identity: cross-client handle misuse, stale generations, unauthorized
control clients, resource-limit and churn cleanup, owner-versus-non-owner stop
behaviour. - Compatibility: desktop / CAT / TCI behaviour preserved, plus positive
independent-client arbitration — not refusal tests alone. - Refusals demonstrated per
Refusalreason, and a mutation check on at
least one new guard (break it, watch the test fail, restore it) per
AGENTS.md. - Socket-free, injected-transport state-machine coverage per the
AGENTS.md
test-layer boundary table. Production backend stop/readback qualification is
distinct from a unit test callingacknowledgeStopped()by hand; any
hardware evidence is arranged explicitly, never through a synthetic firmware
peer.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.