aethersdr / aethersdr/AetherSDR

aetherd — multi-client TX arbiter and daemon transmit grants

Open
#5,690 6 comments 0 reactions 1 assignee View on GitHub

@rfoust is already working on this.

Since Sep 19, 2026.

maintainer-review New Feature protocol safety
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
    admission Refusal reasons made reachable for real clients
  • Wire up the bounded-duration policy that
    docs/aetherd-stage4-tx-coordinator.md describes 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 for TxCoordinator
  • 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:

  1. Maximum continuous operation duration.
  2. Absolute grant lifetime (the parent's bounded arming period).
  3. Explicit operator-presence acknowledgment (parent D13).
  4. 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 Refusal reason, 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 calling acknowledgeStopped() by hand; any
    hardware evidence is arranged explicitly, never through a synthetic firmware
    peer.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.