Skip to content

RR1-T102 Livekit integration and DHI#12

Merged
ucswift merged 3 commits into
masterfrom
develop
Jun 24, 2026
Merged

RR1-T102 Livekit integration and DHI#12
ucswift merged 3 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Jun 24, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added voice, radio, record, and dispatch relay modes.
    • Introduced dispatch tone-out announcements and tune mode for radio audio setup.
    • Added recording support for voice transmissions with local or cloud storage options.
    • Improved native library packaging so voice and radio features are included in published apps.
  • Bug Fixes

    • Improved API client reliability and safer handling when data is unavailable.
    • Updated startup and container behavior for more secure, shell-less execution.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ucswift, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 10 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 06378036-acb3-43d0-b50f-85aaf69b4242

📥 Commits

Reviewing files that changed from the base of the PR and between c615a7c and 70e0712.

📒 Files selected for processing (1)
  • .github/workflows/dotnet.yml
📝 Walkthrough

Walkthrough

Adds the Resgrid.Audio.Voice library, refactors Resgrid API clients to injected instances, adds relay engine dispatch/radio/record modes, and updates Docker, CI, and tooling files.

Changes

LiveKit PTT Voice Subsystem

Layer / File(s) Summary
API client foundation
Providers/Resgrid.Providers.ApiClient/V4/IResgridApiClient.cs, Providers/Resgrid.Providers.ApiClient/V4/ResgridApiClientOptions.cs, Providers/Resgrid.Providers.ApiClient/V4/ResgridV4ApiClient.cs, Providers/Resgrid.Providers.ApiClient/V4/HealthApi.cs
ResgridV4ApiClient becomes an instance client with token-cache isolation support, and HealthApi delegates through the injected client interface.
Call and lookup plumbing
Providers/Resgrid.Providers.ApiClient/V4/CallsApi.cs, Providers/Resgrid.Providers.ApiClient/V4/LookupsApi.cs, Providers/Resgrid.Providers.ApiClient/V4/Models/ActiveCallsModels.cs, Resgrid.Audio.Core/ComService.cs
Call and lookup clients move to injected instances, active-call polling is added, and ComService uses the new API client interfaces.
Voice contracts and PCM helpers
Resgrid.Audio.Voice/Abstractions/*, Resgrid.Audio.Voice/Audio/*, Resgrid.Audio.Voice/Resgrid.Audio.Voice.csproj, Resgrid.Audio.Voice.Tests/Resgrid.Audio.Voice.Tests.csproj, Resgrid.Audio.Voice.Tests/AudioTests.cs
Shared voice-session contracts and PCM helpers define the audio shapes used by the voice library, with tests for mu-law, resampling, and WAV round-trips.
Voice API and channel resolution
Providers/Resgrid.Providers.ApiClient/V4/Models/VoiceModels.cs, Providers/Resgrid.Providers.ApiClient/V4/VoiceApi.cs, Resgrid.Audio.Voice/Connection/*
Voice settings DTOs and the channel-provider code resolve LiveKit channels from voice API responses and seat-limit checks.
LiveKit session lifecycle
Resgrid.Audio.Voice/LiveKit/*
LiveKit transport, room session, publisher, and manager create sessions, forward frames, and publish audio.
DSP and signaling
Resgrid.Audio.Voice/Dsp/*, Resgrid.Audio.Voice.Tests/DspTests.cs
DSP filters, tone detectors, the MDC-1200 codec and decoder, and the emergency alert sink process PCM frames and raise detection events.
Transmission recording
Resgrid.Audio.Voice/Recording/*, Resgrid.Audio.Voice.Tests/RecordingTests.cs
Recording models, stores, logs, settings, and the recorder persist segmented transmissions and metadata.
Tone synthesis and dispatch
Resgrid.Audio.Voice/ToneOut/*, Resgrid.Audio.Voice.Tests/DspTests.cs
Tone profiles, the TTS client, channel resolvers, and the dispatch tone-out service build announcement audio from tones and speech.
Radio hardware and bridge
Resgrid.Audio.Core/Radio/*, Resgrid.Audio.Core/Resgrid.Audio.Core.csproj
Radio settings, device abstractions, PTT and carrier controllers, and RadioBridge wire radio capture and transmit around voice sessions.
Relay engine core and SMTP
Resgrid.Relay.Engine/Abstractions/*, Resgrid.Relay.Engine/Configuration/*, Resgrid.Relay.Engine/Smtp/*, Resgrid.Relay.Engine/LoclxTunnel.cs, Resgrid.Relay.Engine/Resgrid.Relay.Engine.csproj, Resgrid.Audio.Tests/Smtp*.cs, Resgrid.Audio.Tests/Resgrid.Audio.Tests.csproj
Relay state/config types, SMTP lookup and telemetry plumbing, Loclx tunneling, and the relay test project wiring are updated.
Voice modes and console wiring
Resgrid.Relay.Engine/Voice/*, Resgrid.Audio.Relay.Console/Program.cs, Resgrid.Audio.Relay.Console/Resgrid.Audio.Relay.Console.csproj, Resgrid Audio.sln
Dispatch, radio, and record modes plus console commands now orchestrate the voice stack, and the solution and console projects include the new voice projects.
Container and release packaging
Dockerfile, scripts/download-livekit-ffi.sh, .github/workflows/dotnet.yml
The container build and release workflow now download and bundle the LiveKit FFI native library and wait binary.

Developer tooling

Layer / File(s) Summary
.gitignore and CLAUDE.md policy additions
.gitignore, CLAUDE.md
Adds .gitignore entries for local artifacts and a CLAUDE.md graph-tool policy document.

Sequence Diagram(s)

LiveKit session lifecycle

sequenceDiagram
  participant VoiceRoomManager
  participant LiveKitVoiceTransport
  participant LiveKitRoomSession
  participant LiveKitAudioPublisher
  VoiceRoomManager->>LiveKitVoiceTransport: CreateSession(VoiceChannel)
  VoiceRoomManager->>LiveKitRoomSession: ConnectAsync()
  LiveKitRoomSession->>LiveKitRoomSession: TrackReadLoop for subscribed tracks
  LiveKitRoomSession->>LiveKitAudioPublisher: CreatePublisherAsync(trackName)
  LiveKitAudioPublisher->>LiveKitAudioPublisher: WriteAsync / FlushAsync
Loading

Dispatch announcement flow

sequenceDiagram
  participant DispatchVoiceMode
  participant IResgridCallsApi
  participant VoiceRoomManager
  participant IVoiceRoomSession
  participant DispatchToneOutService
  participant IAudioPublisher
  DispatchVoiceMode->>IResgridCallsApi: GetActiveCallsAsync(departmentId)
  DispatchVoiceMode->>VoiceRoomManager: JoinAsync(VoiceChannel)
  VoiceRoomManager-->>DispatchVoiceMode: IVoiceRoomSession
  DispatchVoiceMode->>IVoiceRoomSession: CreatePublisherAsync(trackName)
  DispatchVoiceMode->>DispatchToneOutService: AnnounceAsync(publisher, announcement)
  DispatchToneOutService->>IAudioPublisher: WriteAsync / FlushAsync
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

  • Resgrid/Relay#7 — touches the same CallsApi surface and extends call API behavior in the same area.
  • Resgrid/Relay#11 — also refactors CallsApi and GetCallAsync, which overlaps the call client changes here.

Poem

I hopped through tones and LiveKit light,
and carried calls through day and night.
I nibbled FFI, hop by hop,
then watched the dispatch bells go "pop!"
With recordings stored and voices clear,
this rabbit grins from ear to ear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: LiveKit integration plus a DHI-based container update.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ucswift

ucswift commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (4)
Resgrid.Audio.Voice/Dsp/CtcssDetector.cs (1)

40-49: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Hot-path allocations in Process.

Per-sample List<short>.Add, plus GetRange(...).ToArray() and RemoveRange(0, n) (each an O(n) buffer shift) per block, run on the live receive path. A ring buffer / Span-based sliding window would avoid the per-block allocation and copy. Optional, but this code runs continuously on RF audio.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Audio.Voice/Dsp/CtcssDetector.cs` around lines 40 - 49, The Process
hot path in CtcssDetector is doing per-sample List<short>.Add plus per-block
GetRange(...).ToArray() and RemoveRange(...) shifts, which creates allocations
and copies on the live audio path. Refactor the sliding window logic in Process
to use a ring buffer or Span-based window over pcm/_windowSamples so block
extraction and 50% overlap reuse storage without allocating or shifting the
underlying buffer. Keep the existing Goertzel.NormalizedStrength and _present
update behavior, but change how the block data is buffered and read.
Resgrid.Audio.Voice/Dsp/Mdc1200Decoder.cs (1)

36-53: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Process runs a full-window copy + multi-phase decode every frame once buffered.

After the buffer reaches _windowSamples, every subsequent frame triggers _buffer.ToArray() and a full TryDecode (which itself iterates all bit phases). Combined with per-sample Add and O(n) RemoveRange, this is costly on a continuous RF stream. Consider a ring buffer and/or only attempting decode on a decimated cadence. Functionally fine; optional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Audio.Voice/Dsp/Mdc1200Decoder.cs` around lines 36 - 53,
`Mdc1200Decoder.Process` is doing an expensive full-buffer copy and full
`TryDecode` on every frame once `_buffer` is populated, plus per-sample `Add`
and O(n) `RemoveRange`. Optimize the hot path by switching the backing storage
to a ring buffer or similar sliding window, and consider decoding only on a
decimated cadence instead of every call. Keep the existing behavior of retaining
`_tailSamples` for boundary-spanning packets, but reduce repeated allocations
and scans in `Process` and `Mdc1200Codec.TryDecode`.
Resgrid.Audio.Voice/Recording/SqliteTransmissionLog.cs (1)

68-91: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Dispose the SqliteCommand.

cmd (Line 68) is created without a using, so the command (and its parameter collection) isn't deterministically disposed on each append. Wrap it in await using/using to release native handles promptly on the hot recording path.

🔧 Proposed fix
-				var cmd = conn.CreateCommand();
+				await using var cmd = conn.CreateCommand();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Audio.Voice/Recording/SqliteTransmissionLog.cs` around lines 68 - 91,
The SqliteCommand created in SqliteTransmissionLog.Append/record persistence is
never deterministically disposed, leaving command and parameter resources alive
on the hot path. Update the command lifecycle in the method that builds cmd from
conn.CreateCommand() so it is wrapped in using or await using, and keep the
existing parameter setup and ExecuteNonQueryAsync call inside that scope to
release native handles promptly.
Dockerfile (1)

54-57: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Prefer chown to the runtime user over chmod 0777.

World-writable runtime dirs are broader than needed. If the non-root UID of the DHI base is known, chown-ing the dirs (or 0770) grants the same write access without making them writable by every account in the container.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 54 - 57, The runtime directory setup is too
permissive because it uses world-writable permissions for /app/publish/data and
/app/publish/recordings. Update the Dockerfile’s directory-creation step to
grant write access by changing ownership to the DHI runtime user (or, if
appropriate, use tighter group-based permissions like 0770) instead of chmod
0777, so the non-root container user can write without exposing the dirs to
every account.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/dotnet.yml:
- Around line 126-136: The Add-Ffi helper should fail clearly when the extracted
library is missing, and it should verify the downloaded archive before using it.
Update Add-Ffi to check the Get-ChildItem result before Copy-Item and throw a
descriptive error if no matching FFI library is found, using the Add-Ffi symbol
to locate the change. Also add an integrity check for the Invoke-WebRequest
download step, similar to the ffi-download stage, so the asset.zip contents are
validated before Expand-Archive and Copy-Item run.

In @.gitignore:
- Line 115: The .mcp.json ignore rule is too broad and will match nested files
anywhere in the tree. Update the ignore entry in .gitignore to be anchored to
the repository root so it only applies to the workspace-local config, using the
existing .mcp.json rule location as the reference.

In `@Dockerfile`:
- Around line 87-103: The ffi-download stage currently downloads and installs
liblivekit_ffi.so without verifying integrity, unlike the other pinned downloads
in this repository. Update the ffi-download logic to use a per-arch SHA-256
check before the install step, and keep the existing TARGETARCH and asset
selection flow in sync with the expected checksum values. Locate the change in
the ffi-download stage by the asset selection/curl/unzip/install sequence and
add verification immediately after download and before install.
- Around line 43-44: The Dockerfile currently downloads the generic
docker-compose-wait asset via the ADD in the build stage, which pulls the x86_64
binary and breaks arm64 images. Update the wait download logic to use the
architecture-specific asset based on TARGETARCH, and keep ENTRYPOINT using the
matching binary name so the container works on both amd64 and arm64; use the
existing ffi-download TARGETARCH handling as the pattern and adjust the
wait-related build steps accordingly.

In `@Resgrid.Audio.Relay.Console/Voice/DispatchVoiceMode.cs`:
- Around line 66-83: The dispatch tone-out loop in DispatchVoiceMode currently
adds each CallId to seen before AnnounceAsync succeeds, so a failed announcement
is treated as processed and never retried. Update the logic around
VoiceModeRuntime.FormatCallAnnouncement and service.AnnounceAsync so a call is
only recorded in seen after a successful announce, or explicitly remove the
CallId from seen when AnnounceAsync throws. If needed, handle failures per call
inside the foreach so one bad announcement does not prevent later calls in the
same batch from being processed.

In `@Resgrid.Audio.Relay.Console/Voice/RadioMode.cs`:
- Around line 51-84: The NAudioRadioDevice instance in RadioMode is only
manually disposed at the end, so it can leak if an exception is thrown before
that point. Update the RadioMode flow to scope `device` with a `using` so it is
cleaned up alongside `ptt`, `carrier`, and `bridge`, and remove the trailing
manual `device.Dispose()`; keep the change localized around the
`NAudioRadioDevice`, `RadioBridge`, and `StartAsync` setup path.
- Around line 122-123: The RadioMode configuration parsing ignores the return
value from Enum.TryParse, so invalid values for o.PttMethod and o.CarrierDetect
silently fall back to default enum values. Update the parsing logic in the
RadioMode setup to check the TryParse results for both PttKeyingMethod and
CarrierDetectSource, and when parsing fails, surface a clear warning or error
that includes the invalid setting value and the relevant property name before
continuing or failing fast.
- Around line 147-178: `RadioMode.CreatePtt` and `RadioMode.CreateCarrier` are
always constructing `SerialPttController` and `SerialCarrierDetector` with only
`settings.SerialPort`, which prevents them from sharing the same COM port.
Update these helpers to accept and pass through a shared `SerialPort` instance
(or equivalent shared-port parameter) when serial PTT and serial carrier detect
target the same port, while keeping the existing `settings`, `useDtr`/`useDsr`,
and `logger` wiring intact.

In `@Resgrid.Audio.Relay.Console/Voice/RecordMode.cs`:
- Around line 42-62: Move the setup/wait logic in RecordMode.Execute (the
JoinAsync loop, TransmissionRecorder.Start, and
VoiceModeRuntime.WaitForCancellationAsync) into a try block and put disposal of
recorders, log, and disposableStores in a finally so partial failures or
cancellation still clean up. Use the existing recorders, log, and
disposableStores variables to ensure any successfully created
TransmissionRecorder instances, the optional log, and all disposable store
objects are disposed even if JoinAsync throws mid-loop. Keep the return path
unchanged, but make teardown exception-safe by always running it in the finally.

In `@Resgrid.Audio.Voice/Audio/Resampler.cs`:
- Around line 20-26: Add explicit sample-rate validation in the Resampler
resampling path before computing outLen or step, since inRate and outRate are
currently used in division in Resample without being checked. Update the
Resampler.Resample method to reject zero or negative inRate/outRate values up
front, returning an error or empty result consistently with the existing
behavior, so the later length calculation and step computation cannot divide by
zero.
- Around line 20-25: The Resampler logic currently computes outLen as a long,
but short array allocation requires an int, so update the allocation path in
Resampler to validate outLen is within int.MaxValue before creating the output
buffer. Use the existing outLen calculation in Resampler and, after guarding for
nonpositive values, cast to int only after the bounds check so new short[...]
compiles safely.

In `@Resgrid.Audio.Voice/LiveKit/LiveKitAudioPublisher.cs`:
- Around line 118-138: `LiveKitAudioPublisher.DisposeAsync` currently marks the
publisher as disposed before calling `FlushAsync`, which causes `FlushAsync` to
return immediately and drop any buffered residual audio. Update `DisposeAsync`
to flush pending audio before setting `_disposed` (or perform the flush inline),
and keep the existing `_logger` error handling around the flush and unpublish
steps so the final partial frame is preserved during teardown.

In `@Resgrid.Audio.Voice/LiveKit/LiveKitRoomSession.cs`:
- Around line 48-57: The LiveKitRoomSession connection flow is firing duplicate
“connected” notifications because RaiseConnection(true, "connected") is
triggered both by the _room.Connected event handler and again after ConnectAsync
in LiveKitRoomSession; remove the redundant explicit raise (or the event
handler) so subscribers see a single connected transition, keeping the event
wiring and the ConnectAsync success path consistent.

In `@Resgrid.Audio.Voice/Recording/TransmissionRecorder.cs`:
- Around line 61-69: Guard against a null TrackSid in OnAudioFrame: the current
_segments.GetOrAdd(frame.TrackSid, ...) call will throw if the key is null. Add
an early return near the existing PCM null/empty check in OnAudioFrame so frames
with a null TrackSid are ignored before touching the ConcurrentDictionary.

In `@Resgrid.Audio.Voice/ToneOut/DispatchToneOutService.cs`:
- Around line 70-73: The per-channel publish loop in DispatchToneOutService is
swallowing cancellation by catching Exception and logging it as an error. Update
the catch block in the tone-out publish path to let cancellation flow normally
by handling OperationCanceledException separately (or rethrowing it) and only
logging unexpected failures in the existing _logger.Error path, so normal
shutdown isn’t treated as an error.
- Around line 49-51: Guard the preview logging in DispatchToneOutService so a
null text argument cannot crash the method. In the tone-out path where
_logger?.Information builds the preview from text.Length/text.Substring,
normalize text once at the start of the method (or use a safe fallback string)
and then use that normalized value for both the length check and preview
generation. Update the logging statement and any nearby logic in
DispatchToneOutService to reference the safe variable instead of the raw text
parameter.

In `@Resgrid.Audio.Voice/ToneOut/IDepartmentVoiceResolver.cs`:
- Around line 28-31: The CustomerDepartmentVoiceResolver constructor should
validate its IVoiceChannelProvider dependency immediately instead of allowing a
null to be stored and fail later. Update the constructor in
CustomerDepartmentVoiceResolver to perform a null check on the channels
parameter and throw an appropriate argument exception before assigning it to
_channels, so misconfigured DI is caught at construction time.

In `@Resgrid.Audio.Voice/ToneOut/ToneGenerator.cs`:
- Around line 35-36: Clamp the generated PCM sample before converting it to
short in ToneGenerator.ToneOut.ToneGenerator, where the sample is computed from
value and assigned to samples[i]. The current cast can wrap when
ToneProfile.Amplitude drives the signal beyond the short range, so add a clamp
to the valid short bounds before the assignment. Keep the fix localized to the
sample generation path so the tone output clips safely instead of flipping sign.

---

Nitpick comments:
In `@Dockerfile`:
- Around line 54-57: The runtime directory setup is too permissive because it
uses world-writable permissions for /app/publish/data and
/app/publish/recordings. Update the Dockerfile’s directory-creation step to
grant write access by changing ownership to the DHI runtime user (or, if
appropriate, use tighter group-based permissions like 0770) instead of chmod
0777, so the non-root container user can write without exposing the dirs to
every account.

In `@Resgrid.Audio.Voice/Dsp/CtcssDetector.cs`:
- Around line 40-49: The Process hot path in CtcssDetector is doing per-sample
List<short>.Add plus per-block GetRange(...).ToArray() and RemoveRange(...)
shifts, which creates allocations and copies on the live audio path. Refactor
the sliding window logic in Process to use a ring buffer or Span-based window
over pcm/_windowSamples so block extraction and 50% overlap reuse storage
without allocating or shifting the underlying buffer. Keep the existing
Goertzel.NormalizedStrength and _present update behavior, but change how the
block data is buffered and read.

In `@Resgrid.Audio.Voice/Dsp/Mdc1200Decoder.cs`:
- Around line 36-53: `Mdc1200Decoder.Process` is doing an expensive full-buffer
copy and full `TryDecode` on every frame once `_buffer` is populated, plus
per-sample `Add` and O(n) `RemoveRange`. Optimize the hot path by switching the
backing storage to a ring buffer or similar sliding window, and consider
decoding only on a decimated cadence instead of every call. Keep the existing
behavior of retaining `_tailSamples` for boundary-spanning packets, but reduce
repeated allocations and scans in `Process` and `Mdc1200Codec.TryDecode`.

In `@Resgrid.Audio.Voice/Recording/SqliteTransmissionLog.cs`:
- Around line 68-91: The SqliteCommand created in
SqliteTransmissionLog.Append/record persistence is never deterministically
disposed, leaving command and parameter resources alive on the hot path. Update
the command lifecycle in the method that builds cmd from conn.CreateCommand() so
it is wrapped in using or await using, and keep the existing parameter setup and
ExecuteNonQueryAsync call inside that scope to release native handles promptly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 892445a7-d7d1-4540-bbc9-5b2c322fe7b2

📥 Commits

Reviewing files that changed from the base of the PR and between 3f0fb8a and 3dd664f.

📒 Files selected for processing (74)
  • .github/workflows/dotnet.yml
  • .gitignore
  • CLAUDE.md
  • Dockerfile
  • Providers/Resgrid.Providers.ApiClient/V4/CallsApi.cs
  • Providers/Resgrid.Providers.ApiClient/V4/Models/ActiveCallsModels.cs
  • Providers/Resgrid.Providers.ApiClient/V4/Models/VoiceModels.cs
  • Providers/Resgrid.Providers.ApiClient/V4/VoiceApi.cs
  • Resgrid Audio.sln
  • Resgrid.Audio.Core/Radio/CarrierDetector.cs
  • Resgrid.Audio.Core/Radio/IPttController.cs
  • Resgrid.Audio.Core/Radio/IRadioDevice.cs
  • Resgrid.Audio.Core/Radio/NAudioRadioDevice.cs
  • Resgrid.Audio.Core/Radio/PttControllers.cs
  • Resgrid.Audio.Core/Radio/RadioBridge.cs
  • Resgrid.Audio.Core/Radio/RadioSettings.cs
  • Resgrid.Audio.Core/Resgrid.Audio.Core.csproj
  • Resgrid.Audio.Relay.Console/Configuration/RelayHostOptions.cs
  • Resgrid.Audio.Relay.Console/Configuration/RelayModeOptions.cs
  • Resgrid.Audio.Relay.Console/LoclxTunnel.cs
  • Resgrid.Audio.Relay.Console/Program.cs
  • Resgrid.Audio.Relay.Console/Resgrid.Audio.Relay.Console.csproj
  • Resgrid.Audio.Relay.Console/Voice/DispatchVoiceMode.cs
  • Resgrid.Audio.Relay.Console/Voice/RadioMode.cs
  • Resgrid.Audio.Relay.Console/Voice/RecordMode.cs
  • Resgrid.Audio.Relay.Console/Voice/VoiceModeRuntime.cs
  • Resgrid.Audio.Tests/Resgrid.Audio.Tests.csproj
  • Resgrid.Audio.Voice.Tests/AudioTests.cs
  • Resgrid.Audio.Voice.Tests/DspTests.cs
  • Resgrid.Audio.Voice.Tests/RecordingTests.cs
  • Resgrid.Audio.Voice.Tests/Resgrid.Audio.Voice.Tests.csproj
  • Resgrid.Audio.Voice/Abstractions/AudioFormat.cs
  • Resgrid.Audio.Voice/Abstractions/IAudioPublisher.cs
  • Resgrid.Audio.Voice/Abstractions/IVoiceRoomSession.cs
  • Resgrid.Audio.Voice/Abstractions/IVoiceTransport.cs
  • Resgrid.Audio.Voice/Abstractions/VoiceAudioFrame.cs
  • Resgrid.Audio.Voice/Abstractions/VoiceChannel.cs
  • Resgrid.Audio.Voice/Abstractions/VoiceParticipant.cs
  • Resgrid.Audio.Voice/Audio/MuLaw.cs
  • Resgrid.Audio.Voice/Audio/Resampler.cs
  • Resgrid.Audio.Voice/Audio/WavIo.cs
  • Resgrid.Audio.Voice/Connection/IVoiceChannelProvider.cs
  • Resgrid.Audio.Voice/Connection/ResgridVoiceChannelProvider.cs
  • Resgrid.Audio.Voice/Dsp/AudioFilters.cs
  • Resgrid.Audio.Voice/Dsp/CtcssDetector.cs
  • Resgrid.Audio.Voice/Dsp/EmergencyToneDetector.cs
  • Resgrid.Audio.Voice/Dsp/Goertzel.cs
  • Resgrid.Audio.Voice/Dsp/IEmergencyAlertSink.cs
  • Resgrid.Audio.Voice/Dsp/Mdc1200.cs
  • Resgrid.Audio.Voice/Dsp/Mdc1200Decoder.cs
  • Resgrid.Audio.Voice/Dsp/SquelchGate.cs
  • Resgrid.Audio.Voice/LiveKit/LiveKitAudioPublisher.cs
  • Resgrid.Audio.Voice/LiveKit/LiveKitRoomSession.cs
  • Resgrid.Audio.Voice/LiveKit/LiveKitVoiceTransport.cs
  • Resgrid.Audio.Voice/Recording/ITransmissionStore.cs
  • Resgrid.Audio.Voice/Recording/JsonlTransmissionLog.cs
  • Resgrid.Audio.Voice/Recording/LocalFileTransmissionStore.cs
  • Resgrid.Audio.Voice/Recording/RecorderSettings.cs
  • Resgrid.Audio.Voice/Recording/S3TransmissionStore.cs
  • Resgrid.Audio.Voice/Recording/SqliteTransmissionLog.cs
  • Resgrid.Audio.Voice/Recording/TransmissionRecord.cs
  • Resgrid.Audio.Voice/Recording/TransmissionRecorder.cs
  • Resgrid.Audio.Voice/Resgrid.Audio.Voice.csproj
  • Resgrid.Audio.Voice/ToneOut/DispatchToneOutService.cs
  • Resgrid.Audio.Voice/ToneOut/HostedDepartmentVoiceResolver.cs
  • Resgrid.Audio.Voice/ToneOut/IDepartmentVoiceResolver.cs
  • Resgrid.Audio.Voice/ToneOut/ITextToSpeech.cs
  • Resgrid.Audio.Voice/ToneOut/ResgridTtsClient.cs
  • Resgrid.Audio.Voice/ToneOut/ToneGenerator.cs
  • Resgrid.Audio.Voice/ToneOut/ToneProfile.cs
  • Resgrid.Audio.Voice/ToneOut/TtsSettings.cs
  • Resgrid.Audio.Voice/VoiceRoomManager.cs
  • docker-entrypoint.sh
  • scripts/download-livekit-ffi.sh
💤 Files with no reviewable changes (1)
  • docker-entrypoint.sh

Comment thread .github/workflows/dotnet.yml
Comment thread .gitignore
.vs/
/.idea
/.dual-graph-pro
.mcp.json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Anchor the .mcp.json ignore rule if this is meant to be workspace-local.

As written, it will ignore any .mcp.json anywhere in the tree, which can hide nested fixtures or examples unintentionally. If you only mean the repo-root config, use /.mcp.json.

🔧 Proposed fix
-.mcp.json
+/.mcp.json
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.mcp.json
/.mcp.json
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore at line 115, The .mcp.json ignore rule is too broad and will
match nested files anywhere in the tree. Update the ignore entry in .gitignore
to be anchored to the repository root so it only applies to the workspace-local
config, using the existing .mcp.json rule location as the reference.

Comment thread Dockerfile Outdated
Comment thread Dockerfile
Comment thread Resgrid.Relay.Engine/Voice/DispatchVoiceMode.cs
Comment thread Resgrid.Audio.Voice/Recording/TransmissionRecorder.cs
Comment thread Resgrid.Audio.Voice/ToneOut/DispatchToneOutService.cs
Comment thread Resgrid.Audio.Voice/ToneOut/DispatchToneOutService.cs
Comment thread Resgrid.Audio.Voice/ToneOut/IDepartmentVoiceResolver.cs
Comment thread Resgrid.Audio.Voice/ToneOut/ToneGenerator.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
Dockerfile (1)

53-54: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid world-writable token and recording directories.

./data contains the persisted Resgrid token cache, so chmod -R 0777 lets any container user read or replace tokens and recordings. Prefer setting ownership to the DHI runtime UID/GID and narrowing this to 0700/0770.

Also applies to: 164-164

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 53 - 54, The Dockerfile is making the persisted
token cache and recordings world-writable, which is too permissive for sensitive
runtime data. Update the setup around the publish data/recordings directories to
assign ownership to the DHI runtime UID/GID instead of relying on chmod -R 0777,
and restrict permissions to 0700 for the token cache and 0770 for shared
recordings. Apply the same permission hardening in the other matching Dockerfile
section referenced by the review, using the existing directory setup steps as
the place to change this.
Resgrid.Relay.Engine/LoclxTunnel.cs (2)

31-39: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Return null when tunnel startup fails.

This catch block logs the failure but still returns a non-null LoclxTunnel, so callers can't distinguish “disabled” from “enabled but failed to start.” That makes tunnel startup look successful when no child process is running.

Suggested fix
 			catch (Exception ex)
 			{
 				Cli.Error.WriteLine($"[localxpose] Failed to start tunnel: {ex.Message}");
+				tunnel.Dispose();
+				return null;
 			}
 			return tunnel;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Relay.Engine/LoclxTunnel.cs` around lines 31 - 39, The LoclxTunnel
startup path logs the exception but still returns the tunnel instance, which
makes a failed start look successful. Update the try/catch around
tunnel.Start(options) in the LoclxTunnel constructor/path so that a startup
failure returns null instead of the partially initialized tunnel. Use the
existing Cli.Error.WriteLine logging, and make sure callers can distinguish a
disabled tunnel from one that failed to launch.

52-53: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don't ignore loclx auth login timeouts or failures.

WaitForExit(15000) returns false on timeout, and disposing auth does not kill the hung subprocess. Right now a stuck or failed auth step can leave an orphaned loclx auth login process behind and still continue into tunnel startup as if authentication succeeded.

Suggested fix
 				Environment.SetEnvironmentVariable("LX_ACCESS_TOKEN", token);
 				using var auth = StartLoclx("auth login");
-				auth?.WaitForExit(15000);
+				if (auth == null || !auth.WaitForExit(15000))
+				{
+					auth?.Kill(entireProcessTree: true);
+					throw new TimeoutException("loclx auth login timed out.");
+				}
+
+				if (auth.ExitCode != 0)
+					throw new InvalidOperationException($"loclx auth login failed with exit code {auth.ExitCode}.");
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Relay.Engine/LoclxTunnel.cs` around lines 52 - 53, The auth step in
StartLoclx("auth login") is being ignored when WaitForExit(15000) times out or
fails, so LoclxTunnel can continue as if login succeeded. Update the auth flow
in LoclxTunnel to check the WaitForExit result (and exit code if available), and
treat timeout/failure as a hard error instead of proceeding. If auth does not
complete successfully, explicitly terminate the auth process before disposing it
and abort tunnel startup.
Resgrid.Relay.Engine/Voice/RecordMode.cs (1)

81-88: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard options.S3 before reading Bucket.

When Store is s3 or both and the S3 section is missing, Line 84 throws a NullReferenceException instead of the intended configuration error.

Suggested change
 			if (kind == "s3" || kind == "both")
 			{
 				var s3 = options.S3;
+				if (s3 == null)
+					throw new InvalidOperationException("Recorder S3 store selected but RESGRID__RELAY__Recorder__S3 is not configured.");
 				if (string.IsNullOrWhiteSpace(s3.Bucket))
 					throw new InvalidOperationException("Recorder S3 store selected but RESGRID__RELAY__Recorder__S3__Bucket is not set.");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Relay.Engine/Voice/RecordMode.cs` around lines 81 - 88, Guard
options.S3 before accessing its properties in RecordMode so the configuration
path fails cleanly instead of throwing a NullReferenceException. In the branch
that handles kind == "s3" || kind == "both", add an explicit null/empty check
for options.S3 before reading Bucket, and throw the existing
InvalidOperationException with the same configuration message if the S3 section
is missing. Keep the fix localized to the RecordMode logic around
S3TransmissionStore.Create and the S3 settings validation.
Resgrid.Relay.Engine/Voice/RadioMode.cs (1)

93-121: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

RunTuneAsync is silent with Logger.None.

All of the meter and startup messages on Lines 112-117 are dropped, so tune mode no longer shows the live level display or operator guidance. Pass a real ILogger into RunTuneAsync or write these messages directly to stdout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Relay.Engine/Voice/RadioMode.cs` around lines 93 - 121, RunTuneAsync
is using Logger.None, which suppresses the meter and startup guidance emitted
from the SamplesReceived handler and the logger.Information calls. Update
RadioMode.RunTuneAsync to accept or resolve a real ILogger/Serilog logger, and
use that logger for the live level display and tuning instructions so tune mode
remains visible to the operator.
🧹 Nitpick comments (2)
Resgrid.Relay.Engine/Configuration/RelayModeOptions.cs (1)

53-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer enums or binding-time validation for closed-set options.

PttMethod, CarrierDetect, Store, and Log are all documented as finite value sets, but they're modeled as free-form strings. A config typo here will bind cleanly and only fail much later in the mode runtime. Tightening these to enums now would make the new config surface a lot safer.

Example refactor
+public enum PttMethod
+{
+	Vox,
+	SerialRts,
+	SerialDtr,
+	Cm108
+}
+
+public enum CarrierDetectMode
+{
+	None,
+	SerialCts,
+	SerialDsr,
+	Cm108Gpio
+}
+
 public sealed class RadioModeOptions
 {
-	public string PttMethod { get; set; } = "Vox";
+	public PttMethod PttMethod { get; set; } = PttMethod.Vox;
 ...
-	public string CarrierDetect { get; set; } = "None";
+	public CarrierDetectMode CarrierDetect { get; set; } = CarrierDetectMode.None;
 }

Also applies to: 102-107

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Relay.Engine/Configuration/RelayModeOptions.cs` around lines 53 - 63,
The closed-set relay config fields in RelayModeOptions are still modeled as
free-form strings, so typos will bind silently and fail later in runtime.
Tighten PttMethod and CarrierDetect here, and also the related Store and Log
options mentioned in the same config surface, by switching them to enums or
adding binding-time validation. Update the corresponding configuration model and
any consumers that read these properties so the allowed values are enforced when
options are bound.
Resgrid.Relay.Engine/Voice/DispatchVoiceMode.cs (1)

53-58: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Prune seen to the current active-call set.

seen only grows, so a long-running relay retains every historical CallId for the life of the process. Since each poll already fetches the full active set, intersecting seen with the current ids before announcing avoids unbounded growth.

Suggested change
 			while (!cancellationToken.IsCancellationRequested)
 			{
 				try
 				{
 					var calls = await callsApi.GetActiveCallsAsync(deptId, cancellationToken).ConfigureAwait(false);
+					var activeIds = new HashSet<string>(
+						calls.Where(c => !string.IsNullOrWhiteSpace(c.CallId)).Select(c => c.CallId),
+						StringComparer.Ordinal);
+					seen.IntersectWith(activeIds);
+
 					foreach (var call in calls)
 					{
 						if (string.IsNullOrWhiteSpace(call.CallId) || seen.Contains(call.CallId))
 							continue;

Also applies to: 66-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Relay.Engine/Voice/DispatchVoiceMode.cs` around lines 53 - 58, `seen`
in DispatchVoiceMode only accumulates CallId values and can grow without bound
in a long-running relay. Update the polling flow in DispatchVoiceMode to prune
`seen` against the current result of GetActiveCallsAsync before announcing new
calls, so it only retains ids that are still active. Use the existing `seen`
HashSet and the active-call enumeration in the main polling loop (including the
announcement path around the later block referenced by the review) to remove
stale ids after each poll.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Dockerfile`:
- Around line 121-130: The Dockerfile download step for /wait should require a
checksum on arm64 and fail on HTTP errors. Update the WAIT_SHA256_ARM64 handling
in the RUN block that selects asset/sha so arm64 cannot proceed without a
populated hash, and change the curl invocation in that same block to use
fail-fast behavior. Keep the integrity check tied to the existing sha256sum
validation path so both amd64 and arm64 downloads are always verified.

In `@Providers/Resgrid.Providers.ApiClient/V4/ResgridV4ApiClient.cs`:
- Around line 49-56: `ResgridV4ApiClient.CurrentUserId` is incorrectly falling
back to `DepartmentId` in `SystemApiKey` mode, which causes `ComService` to pass
a non-user value into `SaveCallFileInput.UserId`. Update `CurrentUserId` in
`ResgridV4ApiClient` so it only returns the token-backed user identifier from
`_tokenState` (the `sub` claim) and never substitutes the department id; if
department-scoped uploads are needed, add a separate path or property for that
behavior and keep the user id flow unchanged.

In `@Resgrid.Audio.Core/ComService.cs`:
- Around line 27-33: ComService currently accepts required dependencies without
validation, so wiring mistakes can surface later as NullReferenceException. Add
constructor null guards in ComService for logger, audioProcessor, apiClient,
healthApi, and callsApi so the constructor fails immediately with clear
ArgumentNullException behavior before any of these fields are used.

In `@Resgrid.Relay.Engine/Smtp/SmtpRelayInfrastructure.cs`:
- Around line 123-130: Reject invalid constructor input combinations in
RelayMessageStore before creating CachedLookupsService: if callsClient is
provided without apiClient, and if both apiClient and callsClient are null,
throw an ArgumentException/ArgumentNullException up front. Update the
RelayMessageStore constructor in SmtpRelayInfrastructure to validate these
prerequisites before assigning _callsClient, _lookupCache, and _lookupService so
ResolveDispatchCodes never wires a null lookupsApi path that later fails inside
CachedLookupsService.

In `@Resgrid.Relay.Engine/Voice/RadioMode.cs`:
- Around line 63-64: The RadioMode session setup is disposing the same
RadioBridge instance twice because await using var bridge already handles async
cleanup at scope exit, and the explicit DisposeAsync call later repeats
teardown. Update RadioMode to rely on a single disposal path by removing the
manual DisposeAsync call and keeping the await using scope around the bridge
lifecycle so StartAsync still runs with the same bridge instance.

---

Outside diff comments:
In `@Dockerfile`:
- Around line 53-54: The Dockerfile is making the persisted token cache and
recordings world-writable, which is too permissive for sensitive runtime data.
Update the setup around the publish data/recordings directories to assign
ownership to the DHI runtime UID/GID instead of relying on chmod -R 0777, and
restrict permissions to 0700 for the token cache and 0770 for shared recordings.
Apply the same permission hardening in the other matching Dockerfile section
referenced by the review, using the existing directory setup steps as the place
to change this.

In `@Resgrid.Relay.Engine/LoclxTunnel.cs`:
- Around line 31-39: The LoclxTunnel startup path logs the exception but still
returns the tunnel instance, which makes a failed start look successful. Update
the try/catch around tunnel.Start(options) in the LoclxTunnel constructor/path
so that a startup failure returns null instead of the partially initialized
tunnel. Use the existing Cli.Error.WriteLine logging, and make sure callers can
distinguish a disabled tunnel from one that failed to launch.
- Around line 52-53: The auth step in StartLoclx("auth login") is being ignored
when WaitForExit(15000) times out or fails, so LoclxTunnel can continue as if
login succeeded. Update the auth flow in LoclxTunnel to check the WaitForExit
result (and exit code if available), and treat timeout/failure as a hard error
instead of proceeding. If auth does not complete successfully, explicitly
terminate the auth process before disposing it and abort tunnel startup.

In `@Resgrid.Relay.Engine/Voice/RadioMode.cs`:
- Around line 93-121: RunTuneAsync is using Logger.None, which suppresses the
meter and startup guidance emitted from the SamplesReceived handler and the
logger.Information calls. Update RadioMode.RunTuneAsync to accept or resolve a
real ILogger/Serilog logger, and use that logger for the live level display and
tuning instructions so tune mode remains visible to the operator.

In `@Resgrid.Relay.Engine/Voice/RecordMode.cs`:
- Around line 81-88: Guard options.S3 before accessing its properties in
RecordMode so the configuration path fails cleanly instead of throwing a
NullReferenceException. In the branch that handles kind == "s3" || kind ==
"both", add an explicit null/empty check for options.S3 before reading Bucket,
and throw the existing InvalidOperationException with the same configuration
message if the S3 section is missing. Keep the fix localized to the RecordMode
logic around S3TransmissionStore.Create and the S3 settings validation.

---

Nitpick comments:
In `@Resgrid.Relay.Engine/Configuration/RelayModeOptions.cs`:
- Around line 53-63: The closed-set relay config fields in RelayModeOptions are
still modeled as free-form strings, so typos will bind silently and fail later
in runtime. Tighten PttMethod and CarrierDetect here, and also the related Store
and Log options mentioned in the same config surface, by switching them to enums
or adding binding-time validation. Update the corresponding configuration model
and any consumers that read these properties so the allowed values are enforced
when options are bound.

In `@Resgrid.Relay.Engine/Voice/DispatchVoiceMode.cs`:
- Around line 53-58: `seen` in DispatchVoiceMode only accumulates CallId values
and can grow without bound in a long-running relay. Update the polling flow in
DispatchVoiceMode to prune `seen` against the current result of
GetActiveCallsAsync before announcing new calls, so it only retains ids that are
still active. Use the existing `seen` HashSet and the active-call enumeration in
the main polling loop (including the announcement path around the later block
referenced by the review) to remove stale ids after each poll.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6cbd57a9-37f9-4ddb-879f-e25b73ba605e

📥 Commits

Reviewing files that changed from the base of the PR and between 3dd664f and c615a7c.

📒 Files selected for processing (45)
  • .github/workflows/dotnet.yml
  • Dockerfile
  • Providers/Resgrid.Providers.ApiClient/V4/CallsApi.cs
  • Providers/Resgrid.Providers.ApiClient/V4/HealthApi.cs
  • Providers/Resgrid.Providers.ApiClient/V4/IResgridApiClient.cs
  • Providers/Resgrid.Providers.ApiClient/V4/LookupsApi.cs
  • Providers/Resgrid.Providers.ApiClient/V4/ResgridApiClientOptions.cs
  • Providers/Resgrid.Providers.ApiClient/V4/ResgridV4ApiClient.cs
  • Providers/Resgrid.Providers.ApiClient/V4/VoiceApi.cs
  • Resgrid Audio.sln
  • Resgrid.Audio.Core/ComService.cs
  • Resgrid.Audio.Relay.Console/Program.cs
  • Resgrid.Audio.Relay.Console/Resgrid.Audio.Relay.Console.csproj
  • Resgrid.Audio.Tests/Resgrid.Audio.Tests.csproj
  • Resgrid.Audio.Tests/SmtpDispatchAddressParserTests.cs
  • Resgrid.Audio.Tests/SmtpRelayTelemetryTests.cs
  • Resgrid.Audio.Voice/Audio/Resampler.cs
  • Resgrid.Audio.Voice/Connection/ResgridVoiceChannelProvider.cs
  • Resgrid.Audio.Voice/Dsp/IEmergencyAlertSink.cs
  • Resgrid.Audio.Voice/LiveKit/LiveKitAudioPublisher.cs
  • Resgrid.Audio.Voice/LiveKit/LiveKitRoomSession.cs
  • Resgrid.Audio.Voice/Recording/TransmissionRecorder.cs
  • Resgrid.Audio.Voice/ToneOut/DispatchToneOutService.cs
  • Resgrid.Audio.Voice/ToneOut/IDepartmentVoiceResolver.cs
  • Resgrid.Audio.Voice/ToneOut/ToneGenerator.cs
  • Resgrid.Relay.Engine/Abstractions/ConnectionState.cs
  • Resgrid.Relay.Engine/Abstractions/IRelayService.cs
  • Resgrid.Relay.Engine/Abstractions/IRelayStatus.cs
  • Resgrid.Relay.Engine/Abstractions/RelayServiceState.cs
  • Resgrid.Relay.Engine/Abstractions/RelayStateChangedEventArgs.cs
  • Resgrid.Relay.Engine/Configuration/RelayHostOptions.cs
  • Resgrid.Relay.Engine/Configuration/RelayModeOptions.cs
  • Resgrid.Relay.Engine/LoclxTunnel.cs
  • Resgrid.Relay.Engine/Resgrid.Relay.Engine.csproj
  • Resgrid.Relay.Engine/Smtp/CachedLookupsService.cs
  • Resgrid.Relay.Engine/Smtp/IDispatchLookupCache.cs
  • Resgrid.Relay.Engine/Smtp/NullDispatchLookupCache.cs
  • Resgrid.Relay.Engine/Smtp/RedisDispatchLookupCache.cs
  • Resgrid.Relay.Engine/Smtp/SmtpDispatchAddressParser.cs
  • Resgrid.Relay.Engine/Smtp/SmtpRelayInfrastructure.cs
  • Resgrid.Relay.Engine/Smtp/SmtpTelemetry.cs
  • Resgrid.Relay.Engine/Voice/DispatchVoiceMode.cs
  • Resgrid.Relay.Engine/Voice/RadioMode.cs
  • Resgrid.Relay.Engine/Voice/RecordMode.cs
  • Resgrid.Relay.Engine/Voice/VoiceModeRuntime.cs
✅ Files skipped from review due to trivial changes (5)
  • Resgrid.Relay.Engine/Abstractions/ConnectionState.cs
  • Resgrid.Relay.Engine/Abstractions/RelayServiceState.cs
  • Resgrid.Audio.Tests/SmtpDispatchAddressParserTests.cs
  • Resgrid.Relay.Engine/Smtp/IDispatchLookupCache.cs
  • Resgrid.Relay.Engine/Abstractions/IRelayStatus.cs
🚧 Files skipped from review as they are similar to previous changes (14)
  • Resgrid.Audio.Tests/Resgrid.Audio.Tests.csproj
  • Resgrid.Audio.Voice/ToneOut/IDepartmentVoiceResolver.cs
  • .github/workflows/dotnet.yml
  • Resgrid.Audio.Voice/ToneOut/ToneGenerator.cs
  • Resgrid.Audio.Relay.Console/Resgrid.Audio.Relay.Console.csproj
  • Resgrid.Audio.Voice/Connection/ResgridVoiceChannelProvider.cs
  • Resgrid.Audio.Voice/Dsp/IEmergencyAlertSink.cs
  • Resgrid.Audio.Voice/ToneOut/DispatchToneOutService.cs
  • Resgrid.Audio.Voice/Audio/Resampler.cs
  • Resgrid.Audio.Voice/LiveKit/LiveKitAudioPublisher.cs
  • Resgrid.Audio.Voice/Recording/TransmissionRecorder.cs
  • Resgrid.Audio.Relay.Console/Program.cs
  • Resgrid.Audio.Voice/LiveKit/LiveKitRoomSession.cs
  • Resgrid Audio.sln

Comment thread Dockerfile
Comment on lines +121 to +130
ARG WAIT_SHA256_ARM64=
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& case "${TARGETARCH}" in \
amd64) asset="wait"; sha="${WAIT_SHA256_AMD64}" ;; \
arm64) asset="wait_aarch64"; sha="${WAIT_SHA256_ARM64}" ;; \
*) echo "Unsupported arch: ${TARGETARCH}" >&2 ; exit 1 ;; \
esac \
&& curl -sSL -o /wait \
"https://github.com/ufoscout/docker-compose-wait/releases/download/${WAIT_VERSION}/${asset}" \
&& if [ -n "${sha}" ]; then echo "${sha} /wait" | sha256sum -c - ; fi \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl -fsSL -o "$tmp/wait_aarch64" \
  "https://github.com/ufoscout/docker-compose-wait/releases/download/2.12.1/wait_aarch64"
sha256sum "$tmp/wait_aarch64"

Repository: Resgrid/Relay

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the Dockerfile section around the reported lines.
git ls-files Dockerfile
wc -l Dockerfile
sed -n '110,140p' Dockerfile

# Search for related checksum arguments/usages.
rg -n "WAIT_SHA256|WAIT_VERSION|docker-compose-wait|curl -sSL|curl -fsSL" Dockerfile

Repository: Resgrid/Relay

Length of output: 2836


Require the arm64 checksum and fail on HTTP errors.
Dockerfile:121-130 leaves WAIT_SHA256_ARM64 empty, so arm64 downloads can skip integrity checking, and curl -sSL won’t fail on HTTP error responses. Make the arm64 hash mandatory and switch to curl -fsSL so the downloaded /wait is always validated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 121 - 130, The Dockerfile download step for /wait
should require a checksum on arm64 and fail on HTTP errors. Update the
WAIT_SHA256_ARM64 handling in the RUN block that selects asset/sha so arm64
cannot proceed without a populated hash, and change the curl invocation in that
same block to use fail-fast behavior. Keep the integrity check tied to the
existing sha256sum validation path so both amd64 and arm64 downloads are always
verified.

Comment on lines +49 to +56
public string CurrentUserId
{
get
{
if (_options.GrantType == ResgridAuthGrantType.SystemApiKey && !String.IsNullOrWhiteSpace(_options.DepartmentId))
return _options.DepartmentId;

return _tokenState?.UserId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don’t substitute DepartmentId for CurrentUserId.

Line 53 makes CurrentUserId return a department id in SystemApiKey mode, but ComService passes this value into SaveCallFileInput.UserId. That can misattribute or fail file uploads because a department id is not a user id. Keep CurrentUserId limited to the token sub claim, and model the system-key/department-scoped upload path separately.

As per coding guidelines, "ResgridV4ApiClient handles ... extraction of CurrentUserId from the access token sub claim for file uploads."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Providers/Resgrid.Providers.ApiClient/V4/ResgridV4ApiClient.cs` around lines
49 - 56, `ResgridV4ApiClient.CurrentUserId` is incorrectly falling back to
`DepartmentId` in `SystemApiKey` mode, which causes `ComService` to pass a
non-user value into `SaveCallFileInput.UserId`. Update `CurrentUserId` in
`ResgridV4ApiClient` so it only returns the token-backed user identifier from
`_tokenState` (the `sub` claim) and never substitutes the department id; if
department-scoped uploads are needed, add a separate path or property for that
behavior and keep the user id flow unchanged.

Source: Coding guidelines

Comment on lines +27 to +33
public ComService(Logger logger, AudioProcessor audioProcessor, IResgridApiClient apiClient, IResgridHealthApi healthApi, IResgridCallsApi callsApi)
{
_logger = logger;
_audioProcessor = audioProcessor;
_apiClient = apiClient;
_healthApi = healthApi;
_callsApi = callsApi;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fail fast on missing injected dependencies.

These new required dependencies are dereferenced later; add constructor null guards so wiring errors fail with clear ArgumentNullExceptions instead of later NullReferenceExceptions.

Proposed fix
-			_logger = logger;
-			_audioProcessor = audioProcessor;
-			_apiClient = apiClient;
-			_healthApi = healthApi;
-			_callsApi = callsApi;
+			_logger = logger ?? throw new ArgumentNullException(nameof(logger));
+			_audioProcessor = audioProcessor ?? throw new ArgumentNullException(nameof(audioProcessor));
+			_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
+			_healthApi = healthApi ?? throw new ArgumentNullException(nameof(healthApi));
+			_callsApi = callsApi ?? throw new ArgumentNullException(nameof(callsApi));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ComService(Logger logger, AudioProcessor audioProcessor, IResgridApiClient apiClient, IResgridHealthApi healthApi, IResgridCallsApi callsApi)
{
_logger = logger;
_audioProcessor = audioProcessor;
_apiClient = apiClient;
_healthApi = healthApi;
_callsApi = callsApi;
public ComService(Logger logger, AudioProcessor audioProcessor, IResgridApiClient apiClient, IResgridHealthApi healthApi, IResgridCallsApi callsApi)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_audioProcessor = audioProcessor ?? throw new ArgumentNullException(nameof(audioProcessor));
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
_healthApi = healthApi ?? throw new ArgumentNullException(nameof(healthApi));
_callsApi = callsApi ?? throw new ArgumentNullException(nameof(callsApi));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Audio.Core/ComService.cs` around lines 27 - 33, ComService currently
accepts required dependencies without validation, so wiring mistakes can surface
later as NullReferenceException. Add constructor null guards in ComService for
logger, audioProcessor, apiClient, healthApi, and callsApi so the constructor
fails immediately with clear ArgumentNullException behavior before any of these
fields are used.

Comment on lines +123 to +130
public RelayMessageStore(SmtpRelayOptions options, ISmtpTelemetry telemetry, IResgridApiClient apiClient = null, IResgridCallsClient callsClient = null, IDispatchLookupCache lookupCache = null, IResgridLookupsApi lookupsApi = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_telemetry = telemetry ?? throw new ArgumentNullException(nameof(telemetry));
_callsClient = callsClient ?? new ResgridCallsClient();
_callsClient = callsClient ?? new ResgridCallsClient(apiClient);
_lookupCache = lookupCache ?? CreateLookupCache(options);
_lookupService = new CachedLookupsService(_lookupCache);
lookupsApi ??= apiClient != null ? new LookupsApi(apiClient) : null;
_lookupService = new CachedLookupsService(_lookupCache, lookupsApi);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the constructor prerequisites before wiring the lookup service.

This overload allows callsClient-only construction, but still creates CachedLookupsService with lookupsApi == null. If ResolveDispatchCodes is enabled, the first cache miss will throw from _lookupsApi.Lookup*Async(...). The other invalid combination (apiClient == null && callsClient == null) also fails indirectly in new ResgridCallsClient(apiClient), so both states should be rejected up front.

Suggested fix
 public RelayMessageStore(SmtpRelayOptions options, ISmtpTelemetry telemetry, IResgridApiClient apiClient = null, IResgridCallsClient callsClient = null, IDispatchLookupCache lookupCache = null, IResgridLookupsApi lookupsApi = null)
 {
 	_options = options ?? throw new ArgumentNullException(nameof(options));
 	_telemetry = telemetry ?? throw new ArgumentNullException(nameof(telemetry));
+
+	if (apiClient == null && callsClient == null)
+		throw new ArgumentNullException(nameof(apiClient), "Provide either an apiClient or a callsClient.");
+
+	if (options.ResolveDispatchCodes && apiClient == null && lookupsApi == null)
+		throw new ArgumentException("ResolveDispatchCodes requires either an apiClient or a lookupsApi.", nameof(lookupsApi));
+
 	_callsClient = callsClient ?? new ResgridCallsClient(apiClient);
 	_lookupCache = lookupCache ?? CreateLookupCache(options);
 	lookupsApi ??= apiClient != null ? new LookupsApi(apiClient) : null;
 	_lookupService = new CachedLookupsService(_lookupCache, lookupsApi);
 	_dispatchAddressParser = new SmtpDispatchAddressParser(options);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public RelayMessageStore(SmtpRelayOptions options, ISmtpTelemetry telemetry, IResgridApiClient apiClient = null, IResgridCallsClient callsClient = null, IDispatchLookupCache lookupCache = null, IResgridLookupsApi lookupsApi = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_telemetry = telemetry ?? throw new ArgumentNullException(nameof(telemetry));
_callsClient = callsClient ?? new ResgridCallsClient();
_callsClient = callsClient ?? new ResgridCallsClient(apiClient);
_lookupCache = lookupCache ?? CreateLookupCache(options);
_lookupService = new CachedLookupsService(_lookupCache);
lookupsApi ??= apiClient != null ? new LookupsApi(apiClient) : null;
_lookupService = new CachedLookupsService(_lookupCache, lookupsApi);
public RelayMessageStore(SmtpRelayOptions options, ISmtpTelemetry telemetry, IResgridApiClient apiClient = null, IResgridCallsClient callsClient = null, IDispatchLookupCache lookupCache = null, IResgridLookupsApi lookupsApi = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_telemetry = telemetry ?? throw new ArgumentNullException(nameof(telemetry));
if (apiClient == null && callsClient == null)
throw new ArgumentNullException(nameof(apiClient), "Provide either an apiClient or a callsClient.");
if (options.ResolveDispatchCodes && apiClient == null && lookupsApi == null)
throw new ArgumentException("ResolveDispatchCodes requires either an apiClient or a lookupsApi.", nameof(lookupsApi));
_callsClient = callsClient ?? new ResgridCallsClient(apiClient);
_lookupCache = lookupCache ?? CreateLookupCache(options);
lookupsApi ??= apiClient != null ? new LookupsApi(apiClient) : null;
_lookupService = new CachedLookupsService(_lookupCache, lookupsApi);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Relay.Engine/Smtp/SmtpRelayInfrastructure.cs` around lines 123 - 130,
Reject invalid constructor input combinations in RelayMessageStore before
creating CachedLookupsService: if callsClient is provided without apiClient, and
if both apiClient and callsClient are null, throw an
ArgumentException/ArgumentNullException up front. Update the RelayMessageStore
constructor in SmtpRelayInfrastructure to validate these prerequisites before
assigning _callsClient, _lookupCache, and _lookupService so ResolveDispatchCodes
never wires a null lookupsApi path that later fails inside CachedLookupsService.

Comment on lines +63 to +64
await using var bridge = new RadioBridge(device, ptt, carrier, radioSettings, logger, mdc, emergency, alertSink);
await bridge.StartAsync(session, cancellationToken).ConfigureAwait(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

bridge is disposed twice.

await using var bridge already schedules an async dispose at scope exit, so the explicit DisposeAsync on Line 88 runs a second teardown. If RadioBridge.DisposeAsync is not idempotent, shutdown can fail or double-release resources.

Suggested change
-			await bridge.DisposeAsync().ConfigureAwait(false);
 			return 0;

Also applies to: 88-88

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resgrid.Relay.Engine/Voice/RadioMode.cs` around lines 63 - 64, The RadioMode
session setup is disposing the same RadioBridge instance twice because await
using var bridge already handles async cleanup at scope exit, and the explicit
DisposeAsync call later repeats teardown. Update RadioMode to rely on a single
disposal path by removing the manual DisposeAsync call and keeping the await
using scope around the bridge lifecycle so StartAsync still runs with the same
bridge instance.

@ucswift

ucswift commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is approved.

@ucswift ucswift merged commit df32296 into master Jun 24, 2026
6 of 7 checks passed
This was referenced Jun 25, 2026
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.

1 participant