Skip to content

feat: rsLXST voice, MeshCore flood scopes, security floors, and reliability fixes - #785

Merged
rinchen merged 28 commits into
mainfrom
update
Aug 4, 2026
Merged

feat: rsLXST voice, MeshCore flood scopes, security floors, and reliability fixes#785
rinchen merged 28 commits into
mainfrom
update

Conversation

@rinchen

@rinchen rinchen commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Large update branch covering:

  1. Security — pnpm override floors for open Dependabot/GHSA alerts (undici, postcss, ip-address), plus pnpm/Flatpak packaging bumps.
  2. Features — Reticulum LXST voice calls, MeshCore per-channel Chat flood scopes, Reticulum Chat DM header polish.
  3. Bug fixes — Reticulum (voice lifecycle, Auto→private Direct failover, Chat self-DM, RRC), MeshCore (path hash mode, BLE Blue suppress), Meshtastic MQTT channel map re-sync.

Security

Raises transitive security floors in pnpm-workspace.yaml and bumps the package manager so CI pnpm audit / Dependabot alerts clear after merge to main.

Package Floor (this PR) Advisories addressed
undici / undici-types ^7.29.0 (was ^7.28.0) GHSA-4cwx-7wf7-3272 (cache-directive disclosure / crash), GHSA-8xcm-r25x-g524 (retry interceptor desync), GHSA-v3r7-h72x-cjcm (cookie attribute injection), GHSA-jr45-8vmc-qm54 (Cache-Control whitespace disclosure), GHSA-m8rv-5g2x-5cg5 (CRLF via blob type)
postcss ^8.5.25 (was ^8.5.18; advisory needs ≥8.5.23) GHSA-fxqj-rqcc-2cmp (incomplete sourceMappingURL / arbitrary .map read when from unset)
ip-address ^10.3.1 (new override; mqtt → socks) GHSA-mwp4-54f8-5fhr (Address4 leading-zero octal vs decimal → SSRF / trust-boundary bypass)

Also on this branch:

  • pnpm packageManager 11.15.1 → 11.20.0 (chore: update deps and dedupe + lockfile refresh).
  • Flatpak vendored standalone pnpm archives/sha256s bumped to 11.20.0 so Flatpak builds match packageManager.
  • Existing floors kept/documented in the same overrides block (brace-expansion, fast-uri, tar, ws, js-yaml, markdown-it, tmp, shell-quote, app-builder-lib, etc.).
  • Intended to clear open Dependabot alerts Chat Panel unread notice #74Potential fixes for 2 code quality findings #80 (and the listed undici/postcss/ip-address GHSAs) once merged to the default branch.

Features

Reticulum LXST voice calls

  • Replace the sidecar voice stub with rsLXST TelephonyService: HTTP /api/v1/voice/*, WS voice.*, dedicated /ws/voice PCM path, IPC reticulum:voice* / reticulum:voiceSendAudio / reticulum:onVoiceAudio.
  • Peers + Chat DM Call UI gated by hasLxstVoice, with incoming/outgoing overlay, mute/reject/hangup, capability soft badge.
  • Renderer-owned mic/speaker (Web Audio PCM); dial-identity selection; peer display names in the overlay (not raw hashes).
  • Centered in-call mini-panel (phase / elapsed / TX / RX stats).
  • Outbound progress audio: dial → unique peer-hash DTMF fold → UK double-ring; reorder/busy/fail tones + toasts (connect-fail, busy, no-answer, unexpected drop).
  • Establish-only media: Answer warms AudioContext; mic/TX only after established; sidecar soft-drops pre-establish PCM; Answer mic start coalesced by callGeneration (stops Columba thrash / fatal mid-dial).
  • Lifecycle audit logging (no per-packet noise); teardown gated by link/generation; mute/reject/hangup optimistic UI kept in sync with IPC.
  • Clone/float rsLXST via clone-ratspeak-stack.sh; docs + i18n for voice strings.

MeshCore Chat flood scope per channel

  • Persist Default / Unscoped / named flood scopes independently by Chat viewKey so mesh-wide Public and metro-style channels do not bleed when switching pills.
  • RegionMesh region/scope guidance documented.

Reticulum Chat DM header polish

  • Action order: Probe → Path → Call → Send file, with Path reachable / last-heard chips.
  • Shared outlined cyan chip styling (replacing flat text-links) for Peer details / Probe / Path / Call / Send file.
  • Regression tests for order and style contracts.

Internal cleanup

  • Drop ineffective reticulumPeerStore dynamic imports (static path-slot helpers; silences Vite INEFFECTIVE_DYNAMIC_IMPORT).

Bug fixes

Reticulum Chat / identity

  • Stop opening a self DM on Chat launch: prefer hash-backed senders when own IDs are unknown; filter self from DM tabs; derive own node ids from identity lxmf_hash earlier.
  • Stop sticky self-DM flash during hydrate: persist last-known LXMF hash to seed own-node ids before sidecar identity is ready; gate inferred DM tabs until own is known.

Reticulum LXMF / AutoInterface

  • Demote unhealthy Auto toward private LAN Direct paths: when AutoInterface is down or Direct fails on Auto, prefer live private TCP/UDP hubs for LXMF recovery instead of hanging on a stuck Auto path.
  • Wire prefer_private into LXMF rediscovery; promote opportunistic LXMF/voice triage lines to warn for Columba/developer-bundle classification.

Reticulum voice reliability (post-feature hardening)

  • Fix silent outgoing audio and false fail toasts; gate terminal/media teardown by link + generation.
  • Correlate voice.error by link/generation; treat unanswered outbound terminate so busy tone + toast always fire.
  • Unique peer DTMF fold (full-hash XOR) so peers no longer share prefix-identical melodies; gap before UK ringback.
  • Move PCM off the shared WS bus onto /ws/voice; bound dest→identity cache growth; fix incoming-call UI + session IPC/media races.

RRC

  • #789 — pin chat to bottom with TanStack Virtual.
  • #788 — allow plain-text replies in [whispers] (remember last whisper peer so follow-ups go out as direct NOTICE); pin whisper reply targets against inbound overwrite.

MeshCore

  • Path hash mode no longer resets to 1-byte on connect: companion is source of truth; adopt device-reported path.hash.mode into app settings; AppPanel autosave no longer stamps default mode 0 just from visiting App (stops fighting MeshCore app Experimental 2-byte setting). Dropdown still writes the radio when the user changes it while connected.
  • Sticky MeshCore BLE “Blue” suppress helpers (commitConnectedMeshcoreBleSuppression / preserveOrClearMeshcoreBleSuppression); reconnect contracts updated after extract.
  • Pre-arm BLE MAC suppress from sticky storage so Meshtastic NodeDB cannot revive the companion ghost before BLE attach.

Meshtastic MQTT

  • Re-sync MQTT channel map after RF configure: cold-start MQTT before deviceStore channels left LongFast (etc.) on the wrong slot; re-push topic→index maps when resolved channel configs change.

Commits (origin/main..HEAD)

  • chore: update deps and dedupe
  • feat(reticulum): integrate rsLXST voice calls (includes undici/postcss/ip-address floors)
  • refactor(reticulum): drop ineffective peerStore dynamic imports
  • fix(reticulum): voice call mini-panel, tones, and audit logging
  • fix(reticulum): harden LXST voice after review findings
  • test(reticulum): cover DM header action polish order and styles
  • fix(reticulum): make DM header actions outlined chip buttons
  • feat(meshcore): remember Chat flood scope per channel
  • fix(reticulum): harden LXST voice session lifecycle and docs
  • fix(flatpak): bump vendored pnpm archives to 11.20.0
  • merge: bring origin/main into update
  • fix(reticulum): show peer name in voice dialing overlay
  • fix(reticulum): center voice calling overlay on screen
  • fix(reticulum): stop opening a self DM on Chat launch
  • fix(reticulum): surface Columba LXMF/voice triage logs and stop answer mic thrash
  • fix(rrc): allow plain-text replies in [whispers]
  • fix(rrc): pin chat to bottom with TanStack Virtual
  • fix: re-sync MQTT channel map after RF configure and sticky Blue suppress
  • fix(reticulum): dial tone then ring/busy for outbound LXST voice
  • fix(reticulum): UK double-ring and busy on incomplete outbound voice
  • fix(reticulum): dial–DTMF–ring sequence and short reorder/busy tones
  • fix(reticulum): defer LXST Answer media until established
  • fix(reticulum): unique peer DTMF fold and gap before ringback
  • fix(reticulum): demote unhealthy Auto toward private LAN Direct paths
  • fix(reticulum): harden Auto private failover, voice races, and whisper pin
  • test(meshcore): update BLE suppress source contracts for helper extract
  • fix(reticulum): stop sticky self DM flash during launch hydrate
  • fix(meshcore): stop resetting companion path hash mode on connect

Test plan

Security / packaging

Features

  • pnpm run check:pr (full lint/typecheck/test:run; sidecar path-aware)
  • Full-feature sidecar: pnpm run reticulum:sidecar:test / Clippy (sibling rsLXST at origin/main)
  • Manual voice: Call from Peers + Chat DM; answer/reject/mute/hangup; centered mini-panel + peer name; dial→DTMF→UK ring; busy/fail toasts; Answer does not open mic before Established
  • Manual Chat DM header: Probe / Path / Call / Send file as outlined cyan chips in that order
  • Manual MeshCore: flood scope per channel/DM does not bleed across pills
  • Optional interop: mesh-client ↔ Ratspeak / Python LXST Telephone

Bug fixes

  • MeshCore path hash: set 2-byte in MeshCore app → connect mesh-client → mode stays 2-byte; App dropdown follows device; changing dropdown while connected still writes radio
  • Meshtastic MQTT: cold-start MQTT then RF configure → channel topic map correct; MeshCore BLE MAC suppresses Meshtastic “Blue” ghost
  • RRC: chat stays pinned at bottom while receiving; [whispers] plain follow-ups go to last whisper peer
  • Auto failover: unhealthy Auto / Direct-fail prefers private LAN hub Direct for LXMF
  • Chat launch: no autofocus / sticky flash of a self DM during Reticulum hydrate

rinchen added 3 commits August 3, 2026 16:08
Replace the sidecar voice stub with TelephonyService, Peers/Chat Call UI, and
renderer Web Audio PCM; raise pnpm floors for undici, postcss, and ip-address.
Extract pure path-slot helpers so pathMedium can import the store statically
without a cycle, silencing the Vite INEFFECTIVE_DYNAMIC_IMPORT warning.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Too many files!

This PR contains 119 files, which is 19 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 7a0a974e-bf87-4c63-93ee-ab1a751bfc07

📥 Commits

Reviewing files that changed from the base of the PR and between 158828a and 83bb735.

⛔ Files ignored due to path filters (18)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
  • reticulum-sidecar/Cargo.lock is excluded by !**/*.lock
  • src/renderer/locales/cs/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/de/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/en/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/es/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/fr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/id/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/it/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ja/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ko/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/nl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pt-BR/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ru/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/tr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/uk/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/zh/translation.json is excluded by !src/renderer/locales/**
📒 Files selected for processing (119)
  • AGENTS.md
  • README.md
  • docs/development-environment.md
  • docs/meshcore-meshtastic-parity.md
  • docs/reticulum-sidecar-ipc.md
  • docs/reticulum.md
  • docs/troubleshooting.md
  • electron-builder.yml
  • org.coloradomesh.MeshClient.yml
  • package.json
  • pnpm-workspace.yaml
  • reticulum-sidecar/Cargo.toml
  • reticulum-sidecar/README.md
  • reticulum-sidecar/src/api/mod.rs
  • reticulum-sidecar/src/api/system.rs
  • reticulum-sidecar/src/api/voice.rs
  • reticulum-sidecar/src/api/ws.rs
  • reticulum-sidecar/src/stack/announce_ws_coalesce.rs
  • reticulum-sidecar/src/stack/auto_path_policy.rs
  • reticulum-sidecar/src/stack/live.rs
  • reticulum-sidecar/src/stack/lxmf_delivery.rs
  • reticulum-sidecar/src/stack/lxmf_outbound.rs
  • reticulum-sidecar/src/stack/mod.rs
  • reticulum-sidecar/src/stack/path_failover.rs
  • reticulum-sidecar/src/stack/voice_session.rs
  • scripts/check-i18n-quality.mjs
  • scripts/clone-ratspeak-stack.sh
  • scripts/clone-ratspeak-stack.test.mjs
  • scripts/update.sh
  • scripts/update.test.mjs
  • src/main/index.contract.test.ts
  • src/main/ipc/reticulum-handlers.test.ts
  • src/main/ipc/reticulum-handlers.ts
  • src/main/ipc/reticulum-proxy-rate-limit.contract.test.ts
  • src/main/mqtt-manager.test.ts
  • src/main/mqtt-manager.ts
  • src/main/reticulum-sidecar-manager.test.ts
  • src/main/reticulum-sidecar-manager.ts
  • src/preload/index.ts
  • src/renderer/App.tsx
  • src/renderer/components/AppPanel.test.tsx
  • src/renderer/components/AppPanel.tsx
  • src/renderer/components/ChatComposer.test.tsx
  • src/renderer/components/ChatComposer.tsx
  • src/renderer/components/ChatPanel.test.tsx
  • src/renderer/components/ChatPanel.tsx
  • src/renderer/components/MeshcoreFloodScopeSection.test.tsx
  • src/renderer/components/ReticulumDmPathReachabilityBadge.test.tsx
  • src/renderer/components/ReticulumDmPathReachabilityBadge.tsx
  • src/renderer/components/ReticulumPeerDetailModal.tsx
  • src/renderer/components/ReticulumPeerListPanel.test.tsx
  • src/renderer/components/ReticulumPeerListPanel.tsx
  • src/renderer/components/RrcPanel.test.tsx
  • src/renderer/components/RrcPanel.tsx
  • src/renderer/components/remote/ChatDmRncpControl.test.tsx
  • src/renderer/components/remote/ChatDmRncpControl.tsx
  • src/renderer/components/reticulum/ReticulumVoiceCallButton.test.tsx
  • src/renderer/components/reticulum/ReticulumVoiceCallButton.tsx
  • src/renderer/components/reticulum/ReticulumVoiceOverlay.test.tsx
  • src/renderer/components/reticulum/ReticulumVoiceOverlay.tsx
  • src/renderer/components/rrc/RrcChatView.test.tsx
  • src/renderer/components/rrc/RrcChatView.tsx
  • src/renderer/lib/chatPanelProtocolStorage.test.ts
  • src/renderer/lib/chatPanelProtocolStorage.ts
  • src/renderer/lib/chatUnreadCounts.test.ts
  • src/renderer/lib/chatUnreadCounts.ts
  • src/renderer/lib/connectedMeshcoreBleMac.test.ts
  • src/renderer/lib/connectedMeshcoreBleMac.ts
  • src/renderer/lib/devElectronApiStub.ts
  • src/renderer/lib/drivers/PacketRouter.test.ts
  • src/renderer/lib/meshcorePathHashMode.test.ts
  • src/renderer/lib/meshcorePathHashMode.ts
  • src/renderer/lib/radio/BaseRadioProvider.ts
  • src/renderer/lib/radio/protocol-capabilities.test.ts
  • src/renderer/lib/reticulum/clearReticulumSessionStores.test.ts
  • src/renderer/lib/reticulum/clearReticulumSessionStores.ts
  • src/renderer/lib/reticulum/reticulumPathMedium.ts
  • src/renderer/lib/reticulum/reticulumPathSlots.ts
  • src/renderer/lib/reticulumDmHeaderActions.test.ts
  • src/renderer/lib/reticulumDmHeaderActions.ts
  • src/renderer/lib/reticulumLastSelfLxmfHash.test.ts
  • src/renderer/lib/reticulumLastSelfLxmfHash.ts
  • src/renderer/lib/reticulumOwnNodeIds.test.ts
  • src/renderer/lib/reticulumOwnNodeIds.ts
  • src/renderer/lib/reticulumVoiceAudio.test.ts
  • src/renderer/lib/reticulumVoiceAudio.ts
  • src/renderer/lib/reticulumVoiceCallTones.test.ts
  • src/renderer/lib/reticulumVoiceCallTones.ts
  • src/renderer/lib/reticulumVoiceCapability.test.ts
  • src/renderer/lib/reticulumVoiceCapability.ts
  • src/renderer/lib/reticulumVoiceFeedback.test.ts
  • src/renderer/lib/reticulumVoiceFeedback.ts
  • src/renderer/lib/reticulumVoiceOutcome.test.ts
  • src/renderer/lib/reticulumVoiceOutcome.ts
  • src/renderer/lib/reticulumVoiceRemoteLabel.test.ts
  • src/renderer/lib/reticulumVoiceRemoteLabel.ts
  • src/renderer/lib/reticulumVoiceSession.test.ts
  • src/renderer/lib/reticulumVoiceSession.ts
  • src/renderer/lib/rrcWhisperReply.test.ts
  • src/renderer/lib/rrcWhisperReply.ts
  • src/renderer/lib/timeConstants.ts
  • src/renderer/runtime/useMeshcoreRuntime.path-hash-mode.contract.test.ts
  • src/renderer/runtime/useMeshcoreRuntime.reconnect.test.ts
  • src/renderer/runtime/useMeshcoreRuntime.ts
  • src/renderer/runtime/useMeshtasticRuntime.reconnect-hardening.test.ts
  • src/renderer/runtime/useMeshtasticRuntime.ts
  • src/renderer/runtime/useReticulumRuntime.rrc.test.ts
  • src/renderer/runtime/useReticulumRuntime.ts
  • src/renderer/runtime/useReticulumRuntime.voice.test.ts
  • src/renderer/stores/reticulumPeerStore.ts
  • src/renderer/stores/reticulumVoiceStore.test.ts
  • src/renderer/stores/reticulumVoiceStore.ts
  • src/renderer/stores/rrcSessionStore.test.ts
  • src/renderer/stores/rrcSessionStore.ts
  • src/renderer/vitest.electronApiMock.ts
  • src/shared/electron-api.types.ts
  • src/shared/voice-types.test.ts
  • src/shared/voice-types.ts
  • vitest.config.mts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

The PR integrates LXST voice calls into the Reticulum stack. It adds sidecar telephony APIs, renderer IPC and audio handling, call state management, peer and chat controls, an active-call overlay, capability gating, tests, and rsLXST build support.

Changes

LXST voice integration

Layer / File(s) Summary
rsLXST stack setup and release metadata
AGENTS.md, docs/reticulum.md, scripts/*, reticulum-sidecar/Cargo.toml, package.json, pnpm-workspace.yaml, electron-builder.yml
Build scripts, documentation, release metadata, package settings, and optional sidecar dependencies now include rsLXST and LXST voice support.
Sidecar voice API and telephony session
docs/reticulum-sidecar-ipc.md, reticulum-sidecar/src/api/*, reticulum-sidecar/src/stack/*
The sidecar exposes voice status, call control, mute, and PCM audio operations. VoiceSessionManager connects TelephonyService to StackHandle, identity mapping, and WebSocket events.
Renderer voice contracts, audio, and state
src/shared/*, src/preload/index.ts, src/renderer/lib/reticulumVoice*.ts, src/renderer/stores/reticulumVoiceStore.ts, src/renderer/runtime/*, src/renderer/vitest.electronApiMock.ts
Shared types, IPC methods, PCM conversion, media control, WebSocket routing, and Zustand state now support voice sessions and audio.
Voice controls, overlay, and capabilities
src/renderer/App.tsx, src/renderer/components/*, src/renderer/lib/radio/*, src/renderer/lib/reticulum/clearReticulumSessionStores.ts
Reticulum-only call buttons and a global incoming or active call overlay are available. Session teardown clears voice media and state.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • Colorado-Mesh/mesh-client#719 — Both changes update scripts/clone-ratspeak-stack.sh repository checkout management, including rsLXST handling in this PR.
  • Colorado-Mesh/mesh-client#734 — Both changes modify Reticulum sidecar integration and useReticulumRuntime.ts, although they implement different event flows.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.40% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies rsLXST voice integration as the primary change and also mentions dependency and reliability updates.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch update

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.

@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: 12

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (7)
src/renderer/components/reticulum/ReticulumVoiceOverlay.tsx-62-67 (1)

62-67: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle IPC rejection before discarding the Answer promise.

reticulumVoiceAnswer() awaits the IPC operation without a catch. If the IPC invoke rejects, void reticulumVoiceAnswer() produces an unhandled rejection and gives no failure feedback. Catch and log the error in the session helper, then show the existing call-failed feedback.

As per coding guidelines, “Catches must log, rethrow, or include a // catch-no-log-ok <reason> justification” and “Stateful or I/O code must preserve integrity on failure and document failure points, fallbacks, and relevant logging.”

🤖 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 `@src/renderer/components/reticulum/ReticulumVoiceOverlay.tsx` around lines 62
- 67, Update the reticulumVoiceAnswer session helper used by the answer button
to catch IPC invocation failures, log the caught error, and trigger the existing
call-failed feedback path. Ensure the button’s void-discarded promise no longer
produces an unhandled rejection, while preserving successful answer behavior.

Source: Coding guidelines

src/renderer/components/ReticulumPeerListPanel.tsx-585-587 (1)

585-587: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Disable Call when the Reticulum stack is unavailable.

The button remains enabled when isConnected is false. The call then invokes the voice IPC path while the sidecar is unavailable. Match the ChatPanel behavior.

Proposed fix
-        <ReticulumVoiceCallButton lxmfPeerHash={peer.destination_hash} disabled={busy} />
+        <ReticulumVoiceCallButton
+          lxmfPeerHash={peer.destination_hash}
+          disabled={busy || !isConnected}
+        />
🤖 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 `@src/renderer/components/ReticulumPeerListPanel.tsx` around lines 585 - 587,
Update the ReticulumVoiceCallButton usage in ReticulumPeerListPanel to disable
the button when either busy is true or the Reticulum connection state is
unavailable, matching ChatPanel’s isConnected-based behavior. Preserve the
existing hasLxstVoice conditional and pass the connection state through the
button’s disabled prop.
src/renderer/lib/reticulumVoiceSession.ts-176-183 (1)

176-183: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Surface a capture failure to the user.

When startCaptureAndTx rejects, for example because getUserMedia fails, this catch writes one log line. Playback stays active, so the call continues with receive-only audio and the user sees no indication that the microphone is not working. startCaptureAndTx toasts only in the denied-permission branch.

Push a toast here so the failure is visible.

🐛 Proposed fix
   } catch (e) {
     console.warn('[reticulumVoice] mic capture failed', e);
+    pushAppToast(i18n.t('reticulumVoice.errors.micFailed'), 'error');
   }

Register reticulumVoice.errors.micFailed in the English i18n keys. As per path instructions, "This repo follows AGENTS.md multi-protocol and i18n rules."

🤖 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 `@src/renderer/lib/reticulumVoiceSession.ts` around lines 176 - 183, Update
startReticulumVoiceMediaForActiveCall so its catch block keeps the existing
warning and also displays the reticulumVoice.errors.micFailed toast when
startCaptureAndTx rejects. Register the matching reticulumVoice.errors.micFailed
key in the English i18n resources, following the repository’s i18n conventions.

Source: Path instructions

src/shared/voice-types.ts-57-65 (1)

57-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate role and status against their declared literals.

isVoiceActiveCall accepts arbitrary strings for both fields. The type predicate then stores invalid call states as VoiceActiveCall.

Check role and status against the supported union values before returning true.

As per path instructions, “validate external IPC/API input.”

🤖 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 `@src/shared/voice-types.ts` around lines 57 - 65, Update isVoiceActiveCall to
validate role and status against their declared supported union literals, rather
than accepting arbitrary strings. Preserve the existing checks for link_id and
remote_identity, and return true only when all fields match valid
VoiceActiveCall values.

Source: Path instructions

src/renderer/runtime/useReticulumRuntime.ts-1186-1192 (1)

1186-1192: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a finite integer channel count.

The current condition accepts 1.5 and Infinity. These values are forwarded to audio listeners as channel counts.

Use Number.isInteger(p.channels) && p.channels > 0 before emitting audio. Default invalid values to one channel or reject the event.

As per path instructions, “validate external IPC/API input.”

🤖 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 `@src/renderer/runtime/useReticulumRuntime.ts` around lines 1186 - 1192, Update
the channel validation in the voice.audio handling block before
useReticulumVoiceStore.getState().emitAudio so only finite positive integer
values are accepted via Number.isInteger(p.channels) && p.channels > 0; default
invalid or missing values to one channel, while preserving the existing sample
decoding and emission flow.

Source: Path instructions

scripts/clone-ratspeak-stack.test.mjs-35-41 (1)

35-41: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test the rsLXST checkout contract, not only source strings.

These assertions pass when the strings appear anywhere in clone-ratspeak-stack.sh. They do not prove that ensure_repo receives ${LXST_DIR} and ${RS_LXST_REF}, that pinning checks out the requested revision, or that the reported SHA belongs to the checkout.

Add a temporary local-Git fixture for default and pinned refs. At minimum, assert the complete ensure_repo call and the lxst_sha assignment.

As per coding guidelines: “Ship a passing test for behavioral changes.”

🤖 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 `@scripts/clone-ratspeak-stack.test.mjs` around lines 35 - 41, Replace the
source-string-only assertions in the rsLXST test with behavioral tests using a
temporary local Git repository fixture covering both default and pinned refs.
Execute the clone script and verify the complete ensure_repo invocation uses
${LXST_DIR} and ${RS_LXST_REF}, pinning checks out the requested revision, and
the lxst_sha assignment reports the SHA of the resulting checkout.

Source: Coding guidelines

scripts/clone-ratspeak-stack.sh-114-117 (1)

114-117: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report the branch that was actually selected.

ensure_repo falls back to origin/master when origin/main is unavailable, but lxst_mode is always set to floated origin/main. The final status can therefore report the wrong checkout mode for rsLXST.

Derive the mode from the selected reference or return the selected reference from ensure_repo.

Suggested fix
-lxst_mode='floated origin/main'
+if git -C "${LXST_DIR}" rev-parse --verify --quiet 'origin/main' >/dev/null; then
+  lxst_mode='floated origin/main'
+else
+  lxst_mode='floated origin/master'
+fi
🤖 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 `@scripts/clone-ratspeak-stack.sh` around lines 114 - 117, Update the rsLXST
handling around ensure_repo and lxst_mode so the reported mode reflects the
reference actually selected, including the origin/master fallback when
origin/main is unavailable. Derive lxst_mode from ensure_repo’s selected
reference or have ensure_repo return that reference, while preserving the
existing status reporting for the other repositories.
🧹 Nitpick comments (3)
src/renderer/lib/reticulumVoiceAudio.test.ts (1)

26-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for resampling and hash normalization.

The packing test only exercises the identity case, where the input rate equals LXST_QUALITY_HIGH_SAMPLE_RATE_HZ and the input length equals one frame. The resample arithmetic in packQualityHighFrame is therefore untested. The identity tests also skip normalization and rejection.

Add cases for a non-48 kHz input rate, a multichannel mix-down, an uppercase identity hash, and an invalid-length identity hash.

💚 Proposed additional cases
+  it('upsamples 24k mono to a QualityHigh frame', () => {
+    const input = new Float32Array(LXST_QUALITY_HIGH_FRAME_SAMPLES / 2).fill(0.25);
+    const packed = packQualityHighFrame(input, 24_000, 1);
+    expect(packed?.length).toBe(LXST_QUALITY_HIGH_FRAME_SAMPLES);
+    expect(packed?.[0]).toBeCloseTo(0.25);
+    expect(packed?.[LXST_QUALITY_HIGH_FRAME_SAMPLES - 1]).toBeCloseTo(0.25);
+  });
+
+  it('mixes stereo down to mono', () => {
+    const input = new Float32Array(LXST_QUALITY_HIGH_FRAME_SAMPLES * 2);
+    for (let i = 0; i < LXST_QUALITY_HIGH_FRAME_SAMPLES; i += 1) {
+      input[i * 2] = 1;
+      input[i * 2 + 1] = -1;
+    }
+    expect(packQualityHighFrame(input, 48_000, 2)?.[0]).toBeCloseTo(0);
+  });
+
+  it('normalizes case and rejects malformed identity hashes', () => {
+    const upper = 'A'.repeat(32);
+    expect(resolveVoiceDialIdentityHash({ identityHash: upper })).toEqual({
+      identityHash: 'a'.repeat(32),
+    });
+    expect(resolveVoiceDialIdentityHash({ identityHash: 'a'.repeat(31) })).toEqual({
+      errorKey: 'reticulumVoice.errors.noIdentity',
+    });
+  });
🤖 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 `@src/renderer/lib/reticulumVoiceAudio.test.ts` around lines 26 - 55, Extend
the tests for packQualityHighFrame and resolveVoiceDialIdentityHash to cover
non-48 kHz resampling, multichannel mix-down, uppercase identity-hash
normalization, and rejection of identity hashes with invalid lengths. Assert the
expected packed output shape or behavior and normalized lowercase hash result
while preserving the existing identity and fallback cases.
src/renderer/lib/reticulumVoiceAudio.ts (1)

12-45: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Replace per-byte string building in the base64 codec.

encodeF32LeBase64 and decodeF32LeBase64 build an intermediate binary string one code point at a time. Each QualityHigh frame is 2,880 samples, so each call runs 11,520 string concatenations. startCaptureAndTx in src/renderer/lib/reticulumVoiceSession.ts encodes about 33 frames per second, and the runtime decodes inbound frames at the same rate. Use a typed-array view plus chunked conversion instead.

Note that String.fromCharCode(...bytes) in one call would exceed the argument limit at this size, so keep the chunk loop.

♻️ Proposed chunked conversion
 export function encodeF32LeBase64(samples: Float32Array | number[]): string {
-  const bytes = new Uint8Array(samples.length * 4);
-  const view = new DataView(bytes.buffer);
-  for (let i = 0; i < samples.length; i += 1) {
-    view.setFloat32(i * 4, samples[i] ?? 0, true);
-  }
-  let binary = '';
-  for (const b of bytes) {
-    binary += String.fromCodePoint(b);
-  }
-  return btoa(binary);
+  const floats = samples instanceof Float32Array ? samples : Float32Array.from(samples);
+  const bytes = new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength);
+  const parts: string[] = [];
+  const CHUNK = 8192;
+  for (let i = 0; i < bytes.length; i += CHUNK) {
+    parts.push(String.fromCharCode(...bytes.subarray(i, i + CHUNK)));
+  }
+  return btoa(parts.join(''));
 }

The Float32Array view is only valid on little-endian hosts, which is every platform this app targets. If you prefer to keep the explicit DataView writes, keep them and change only the string-building loop.

🤖 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 `@src/renderer/lib/reticulumVoiceAudio.ts` around lines 12 - 45, Replace the
per-byte string construction in encodeF32LeBase64 and decodeF32LeBase64 with
chunked typed-array-to-string conversion, using String.fromCharCode on bounded
byte slices rather than one call over the full buffer. Preserve the existing
little-endian float encoding/decoding and malformed-input behavior, and keep the
chunk loop to avoid argument-limit failures.
src/shared/voice-types.test.ts (1)

18-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add negative cases for wrong field types and unexpected enum values.

The rejection cases only cover null and missing fields. Two gaps remain:

  • No case passes a wrong-typed field, for example { available: 'yes', enabled: true }. That is the shape a schema drift in the sidecar would produce.
  • No case passes an unexpected role or status. ReticulumVoiceOverlay.tsx branches on status === 'ringing' and status === 'established', so whether isVoiceActiveCall restricts those values to an allowed set is behavior worth pinning.
💚 Proposed additional cases
   it('rejects garbage status', () => {
     expect(isVoiceStatusResponse(null)).toBe(false);
     expect(isVoiceStatusResponse({ available: true })).toBe(false);
+    expect(isVoiceStatusResponse({ available: 'yes', enabled: true })).toBe(false);
+    expect(isVoiceStatusResponse([])).toBe(false);
   });
@@
   it('rejects incomplete active_call', () => {
     expect(isVoiceActiveCall({ link_id: 'x' })).toBe(false);
+    expect(
+      isVoiceActiveCall({
+        link_id: 'a'.repeat(32),
+        remote_identity: 'b'.repeat(32),
+        role: 'sideways',
+        status: 'ringing',
+      }),
+    ).toBe(false);
   });

If isVoiceActiveCall does not currently constrain role and status, tighten the guard rather than the test.

🤖 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 `@src/shared/voice-types.test.ts` around lines 18 - 36, Expand the rejection
tests in voice-types.test.ts to cover wrong-typed fields such as available being
a string, plus unsupported role and status values in isVoiceActiveCall. If the
guard currently accepts arbitrary enum strings, tighten isVoiceActiveCall to
allow only the defined role and status values used by ReticulumVoiceOverlay,
while preserving valid active_call acceptance.
🤖 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 `@reticulum-sidecar/Cargo.toml`:
- Around line 98-106: Update the Cargo dependency setup for lxst-core and
lxst-telephony so stub builds resolve without requiring the ../../rsLXST
checkout, using an available stub crate or generated manifest. Add build
validation for both environments: a stub configuration with the sibling checkout
absent and a full-stack configuration with it present, while preserving the
existing optional feature behavior.

In `@reticulum-sidecar/src/stack/voice_session.rs`:
- Around line 213-224: Update remember_identity_for_dest to use a bounded-cache
insertion strategy matching insert_display_name_bounded and the
display_name_cache configuration, evicting an existing entry when the
destination-to-identity map reaches its configured capacity. Preserve the
current normalization and 32-character validation, and keep the cache bounded
for every announce or path-response insertion.
- Around line 345-363: Update the OpusFramesReceived handling in voice session
event processing so voice.audio no longer uses shared.event_tx or the broadcast
bus backing /ws. Route encoded audio through a dedicated high-throughput channel
or endpoint, following the existing high-rate packet tap stream pattern, while
preserving the current per-frame payload fields and encoding.

In `@src/renderer/components/reticulum/ReticulumVoiceOverlay.tsx`:
- Around line 34-44: Update the incoming-call state flow used by applyIncoming
and subsequent activeCall status updates so incomingCall is cleared once the
call progresses beyond incoming ringing, allowing the active-call bar to
display. Preserve showIncoming for genuinely ringing or available incoming
calls, and add a regression test covering answer followed by connecting or
established status that verifies the dialog closes.

In `@src/renderer/lib/reticulumVoiceSession.ts`:
- Around line 121-122: Update the capture setup around processor.connect so the
ScriptProcessorNode remains connected through a muted GainNode rather than
directly to ctx.destination, preventing microphone playback. Track the GainNode
with the other capture nodes and disconnect it in stopCapture() alongside the
existing capture cleanup.
- Around line 209-233: Update reticulumVoiceAnswer, reticulumVoiceReject,
reticulumVoiceHangup, and reticulumVoiceSetMuted to catch rejected IPC calls,
report failures through the existing voice error toast/i18n patterns, and
preserve local state integrity. Keep clearCall() unconditional after
reject/hangup attempts, and ensure setMicrophoneMuted(muted) runs even when
mute() fails; retain media teardown before reject/hangup and register the
reticulumVoice.errors.muteFailed English i18n key.
- Around line 149-174: Update startPlayback to maintain a monotonic playback
cursor and schedule each AudioBufferSourceNode at the cursor rather than
starting immediately at ctx.currentTime; initialize or clamp the cursor so late
arrivals do not create gaps or overlaps, then advance it by buffer.duration
after each frame. Reset playbackCursor in stopPlayback alongside the existing
playback cleanup.
- Around line 91-111: Guard startCaptureAndTx against overlapping invocations
with a capture generation token. Capture the generation before asynchronous
work, invalidate stale starts after each await and before assigning
captureStream, captureCtx, captureSource, or captureProcessor, and clean up any
stream or AudioContext acquired by a stale invocation. Increment the generation
in stopCapture so an explicit stop cancels in-flight startup.
- Around line 185-207: Update reticulumVoiceCallPeer to validate the raw result
from api.getStatus() with the exported isVoiceStatusResponse guard before
passing it to applyStatus; treat invalid status data as a failed call and show
the existing call-failed error toast. Wrap both api.getStatus() and api.call()
in rejection handling so IPC failures are converted to an error toast rather
than escaping as unhandled promise rejections, while preserving the existing
status, identity-resolution, and successful media-start flows.

In `@src/renderer/runtime/useReticulumRuntime.voice.test.ts`:
- Around line 11-24: Replace the source-text assertions in the voice routing
test with a behavioral test that registers the runtime’s onEvent callback
through the Electron mock, dispatches each voice.update, voice.incoming,
voice.terminated, voice.error, and voice.audio event, and verifies the
corresponding voice-store transitions plus decoded audio delivery through
emitAudio.

In `@src/renderer/stores/reticulumVoiceStore.ts`:
- Around line 55-57: Update applyStatus and snapshot handling to derive
incomingCall from the newly resolved activeCall, retaining it only when the call
is incoming with ringing or available status and clearing it for established,
active, or null calls. Add an assertion covering this transition, while
preserving existing activeCall and lastError behavior.
- Around line 104-118: Update applyTerminated to return the existing state
immediately when linkId is missing or empty, before clearing activeCall or
incomingCall; retain the existing stale-call comparison for valid IDs. Add a
test covering a termination event without a link ID and verify the current call
state remains unchanged.

---

Other comments:
In `@scripts/clone-ratspeak-stack.sh`:
- Around line 114-117: Update the rsLXST handling around ensure_repo and
lxst_mode so the reported mode reflects the reference actually selected,
including the origin/master fallback when origin/main is unavailable. Derive
lxst_mode from ensure_repo’s selected reference or have ensure_repo return that
reference, while preserving the existing status reporting for the other
repositories.

In `@scripts/clone-ratspeak-stack.test.mjs`:
- Around line 35-41: Replace the source-string-only assertions in the rsLXST
test with behavioral tests using a temporary local Git repository fixture
covering both default and pinned refs. Execute the clone script and verify the
complete ensure_repo invocation uses ${LXST_DIR} and ${RS_LXST_REF}, pinning
checks out the requested revision, and the lxst_sha assignment reports the SHA
of the resulting checkout.

In `@src/renderer/components/reticulum/ReticulumVoiceOverlay.tsx`:
- Around line 62-67: Update the reticulumVoiceAnswer session helper used by the
answer button to catch IPC invocation failures, log the caught error, and
trigger the existing call-failed feedback path. Ensure the button’s
void-discarded promise no longer produces an unhandled rejection, while
preserving successful answer behavior.

In `@src/renderer/components/ReticulumPeerListPanel.tsx`:
- Around line 585-587: Update the ReticulumVoiceCallButton usage in
ReticulumPeerListPanel to disable the button when either busy is true or the
Reticulum connection state is unavailable, matching ChatPanel’s
isConnected-based behavior. Preserve the existing hasLxstVoice conditional and
pass the connection state through the button’s disabled prop.

In `@src/renderer/lib/reticulumVoiceSession.ts`:
- Around line 176-183: Update startReticulumVoiceMediaForActiveCall so its catch
block keeps the existing warning and also displays the
reticulumVoice.errors.micFailed toast when startCaptureAndTx rejects. Register
the matching reticulumVoice.errors.micFailed key in the English i18n resources,
following the repository’s i18n conventions.

In `@src/renderer/runtime/useReticulumRuntime.ts`:
- Around line 1186-1192: Update the channel validation in the voice.audio
handling block before useReticulumVoiceStore.getState().emitAudio so only finite
positive integer values are accepted via Number.isInteger(p.channels) &&
p.channels > 0; default invalid or missing values to one channel, while
preserving the existing sample decoding and emission flow.

In `@src/shared/voice-types.ts`:
- Around line 57-65: Update isVoiceActiveCall to validate role and status
against their declared supported union literals, rather than accepting arbitrary
strings. Preserve the existing checks for link_id and remote_identity, and
return true only when all fields match valid VoiceActiveCall values.

---

Nitpick comments:
In `@src/renderer/lib/reticulumVoiceAudio.test.ts`:
- Around line 26-55: Extend the tests for packQualityHighFrame and
resolveVoiceDialIdentityHash to cover non-48 kHz resampling, multichannel
mix-down, uppercase identity-hash normalization, and rejection of identity
hashes with invalid lengths. Assert the expected packed output shape or behavior
and normalized lowercase hash result while preserving the existing identity and
fallback cases.

In `@src/renderer/lib/reticulumVoiceAudio.ts`:
- Around line 12-45: Replace the per-byte string construction in
encodeF32LeBase64 and decodeF32LeBase64 with chunked typed-array-to-string
conversion, using String.fromCharCode on bounded byte slices rather than one
call over the full buffer. Preserve the existing little-endian float
encoding/decoding and malformed-input behavior, and keep the chunk loop to avoid
argument-limit failures.

In `@src/shared/voice-types.test.ts`:
- Around line 18-36: Expand the rejection tests in voice-types.test.ts to cover
wrong-typed fields such as available being a string, plus unsupported role and
status values in isVoiceActiveCall. If the guard currently accepts arbitrary
enum strings, tighten isVoiceActiveCall to allow only the defined role and
status values used by ReticulumVoiceOverlay, while preserving valid active_call
acceptance.
🪄 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: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 42b70207-61da-4c76-b70d-688ded354ebc

📥 Commits

Reviewing files that changed from the base of the PR and between 5fdc15b and 69fbf56.

⛔ Files ignored due to path filters (18)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !**/pnpm-lock.yaml
  • reticulum-sidecar/Cargo.lock is excluded by !**/*.lock
  • src/renderer/locales/cs/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/de/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/en/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/es/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/fr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/id/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/it/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ja/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ko/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/nl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pl/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/pt-BR/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/ru/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/tr/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/uk/translation.json is excluded by !src/renderer/locales/**
  • src/renderer/locales/zh/translation.json is excluded by !src/renderer/locales/**
📒 Files selected for processing (45)
  • AGENTS.md
  • docs/reticulum-sidecar-ipc.md
  • docs/reticulum.md
  • electron-builder.yml
  • package.json
  • pnpm-workspace.yaml
  • reticulum-sidecar/Cargo.toml
  • reticulum-sidecar/src/api/mod.rs
  • reticulum-sidecar/src/api/system.rs
  • reticulum-sidecar/src/api/voice.rs
  • reticulum-sidecar/src/stack/announce_ws_coalesce.rs
  • reticulum-sidecar/src/stack/live.rs
  • reticulum-sidecar/src/stack/mod.rs
  • reticulum-sidecar/src/stack/voice_session.rs
  • scripts/clone-ratspeak-stack.sh
  • scripts/clone-ratspeak-stack.test.mjs
  • scripts/update.sh
  • scripts/update.test.mjs
  • src/preload/index.ts
  • src/renderer/App.tsx
  • src/renderer/components/ChatPanel.tsx
  • src/renderer/components/ReticulumPeerDetailModal.tsx
  • src/renderer/components/ReticulumPeerListPanel.test.tsx
  • src/renderer/components/ReticulumPeerListPanel.tsx
  • src/renderer/components/reticulum/ReticulumVoiceCallButton.test.tsx
  • src/renderer/components/reticulum/ReticulumVoiceCallButton.tsx
  • src/renderer/components/reticulum/ReticulumVoiceOverlay.test.tsx
  • src/renderer/components/reticulum/ReticulumVoiceOverlay.tsx
  • src/renderer/lib/radio/BaseRadioProvider.ts
  • src/renderer/lib/radio/protocol-capabilities.test.ts
  • src/renderer/lib/reticulum/clearReticulumSessionStores.ts
  • src/renderer/lib/reticulum/reticulumPathMedium.ts
  • src/renderer/lib/reticulum/reticulumPathSlots.ts
  • src/renderer/lib/reticulumVoiceAudio.test.ts
  • src/renderer/lib/reticulumVoiceAudio.ts
  • src/renderer/lib/reticulumVoiceSession.ts
  • src/renderer/runtime/useReticulumRuntime.ts
  • src/renderer/runtime/useReticulumRuntime.voice.test.ts
  • src/renderer/stores/reticulumPeerStore.ts
  • src/renderer/stores/reticulumVoiceStore.test.ts
  • src/renderer/stores/reticulumVoiceStore.ts
  • src/renderer/vitest.electronApiMock.ts
  • src/shared/electron-api.types.ts
  • src/shared/voice-types.test.ts
  • src/shared/voice-types.ts
💤 Files with no reviewable changes (1)
  • reticulum-sidecar/src/api/system.rs

Comment thread reticulum-sidecar/Cargo.toml
Comment thread reticulum-sidecar/src/stack/voice_session.rs
Comment thread reticulum-sidecar/src/stack/voice_session.rs
Comment on lines +34 to +44
const showIncoming =
incoming?.role === 'incoming' &&
(incoming.status === 'ringing' || incoming.status === 'available');

const showInCall =
active != null &&
!showIncoming &&
(active.status === 'calling' ||
active.status === 'connecting' ||
active.status === 'established' ||
active.status === 'ringing');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear the incoming dialog when the call progresses.

applyIncoming() sets both store fields. Later status updates can change only activeCall to connecting or established while incomingCall remains ringing. This condition still shows the incoming dialog and hides the active-call bar after Answer.

Clear incomingCall when the active call leaves incoming ringing. Alternatively, derive showIncoming from active. Add a regression test that answers an incoming call, applies a connecting or established update, and verifies that the dialog closes.

Proposed local guard
-  const showIncoming =
-    incoming?.role === 'incoming' &&
-    (incoming.status === 'ringing' || incoming.status === 'available');
+  const showIncoming =
+    active?.role === 'incoming' &&
+    (active.status === 'ringing' || active.status === 'available');
📝 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
const showIncoming =
incoming?.role === 'incoming' &&
(incoming.status === 'ringing' || incoming.status === 'available');
const showInCall =
active != null &&
!showIncoming &&
(active.status === 'calling' ||
active.status === 'connecting' ||
active.status === 'established' ||
active.status === 'ringing');
const showIncoming =
active?.role === 'incoming' &&
(active.status === 'ringing' || active.status === 'available');
const showInCall =
active != null &&
!showIncoming &&
(active.status === 'calling' ||
active.status === 'connecting' ||
active.status === 'established' ||
active.status === 'ringing');
🤖 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 `@src/renderer/components/reticulum/ReticulumVoiceOverlay.tsx` around lines 34
- 44, Update the incoming-call state flow used by applyIncoming and subsequent
activeCall status updates so incomingCall is cleared once the call progresses
beyond incoming ringing, allowing the active-call bar to display. Preserve
showIncoming for genuinely ringing or available incoming calls, and add a
regression test covering answer followed by connecting or established status
that verifies the dialog closes.

Comment thread src/renderer/lib/reticulumVoiceSession.ts Outdated
Comment thread src/renderer/lib/reticulumVoiceSession.ts Outdated
Comment on lines +209 to +233
export async function reticulumVoiceAnswer(): Promise<void> {
const resp = await window.electronAPI.reticulum.voice.answer();
if (!resp.ok) {
pushAppToast(resp.error || i18n.t('reticulumVoice.errors.callFailed'), 'error');
return;
}
await startReticulumVoiceMediaForActiveCall();
}

export async function reticulumVoiceReject(): Promise<void> {
stopReticulumVoiceMedia();
await window.electronAPI.reticulum.voice.reject();
useReticulumVoiceStore.getState().clearCall();
}

export async function reticulumVoiceHangup(): Promise<void> {
stopReticulumVoiceMedia();
await window.electronAPI.reticulum.voice.hangup();
useReticulumVoiceStore.getState().clearCall();
}

export async function reticulumVoiceSetMuted(muted: boolean): Promise<void> {
await window.electronAPI.reticulum.voice.mute({ muted });
useReticulumVoiceStore.getState().setMicrophoneMuted(muted);
}

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

Preserve state integrity when the voice IPC calls fail.

None of these four functions handles a rejected IPC promise, and each one updates local state only after the call resolves. The failure modes differ:

  • reticulumVoiceAnswer (Line 210): a rejection escapes as an unhandled rejection. Callers use void reticulumVoiceAnswer(), so the user sees no error.
  • reticulumVoiceReject and reticulumVoiceHangup (Lines 220, 226): media stops first, which is correct, but clearCall() is skipped when the call rejects. The overlay then stays visible with stale call state and no dismiss path.
  • reticulumVoiceSetMuted (Line 231): when mute() rejects, setMicrophoneMuted never runs. The capture path in startCaptureAndTx reads microphoneMuted from the store, so local capture gating diverges from the sidecar mute state.

Wrap each call and keep the local teardown unconditional.

🛡️ Proposed fix
 export async function reticulumVoiceAnswer(): Promise<void> {
-  const resp = await window.electronAPI.reticulum.voice.answer();
-  if (!resp.ok) {
-    pushAppToast(resp.error || i18n.t('reticulumVoice.errors.callFailed'), 'error');
-    return;
-  }
+  try {
+    const resp = await window.electronAPI.reticulum.voice.answer();
+    if (!resp.ok) {
+      pushAppToast(resp.error || i18n.t('reticulumVoice.errors.callFailed'), 'error');
+      return;
+    }
+  } catch (e) {
+    console.warn('[reticulumVoice] answer failed', e);
+    pushAppToast(i18n.t('reticulumVoice.errors.callFailed'), 'error');
+    return;
+  }
   await startReticulumVoiceMediaForActiveCall();
 }

 export async function reticulumVoiceReject(): Promise<void> {
   stopReticulumVoiceMedia();
-  await window.electronAPI.reticulum.voice.reject();
-  useReticulumVoiceStore.getState().clearCall();
+  try {
+    await window.electronAPI.reticulum.voice.reject();
+  } catch (e) {
+    console.warn('[reticulumVoice] reject failed', e);
+  } finally {
+    // Always drop local call state so the overlay cannot latch.
+    useReticulumVoiceStore.getState().clearCall();
+  }
 }

 export async function reticulumVoiceHangup(): Promise<void> {
   stopReticulumVoiceMedia();
-  await window.electronAPI.reticulum.voice.hangup();
-  useReticulumVoiceStore.getState().clearCall();
+  try {
+    await window.electronAPI.reticulum.voice.hangup();
+  } catch (e) {
+    console.warn('[reticulumVoice] hangup failed', e);
+  } finally {
+    useReticulumVoiceStore.getState().clearCall();
+  }
 }

 export async function reticulumVoiceSetMuted(muted: boolean): Promise<void> {
-  await window.electronAPI.reticulum.voice.mute({ muted });
-  useReticulumVoiceStore.getState().setMicrophoneMuted(muted);
+  try {
+    const resp = await window.electronAPI.reticulum.voice.mute({ muted });
+    // Trust the sidecar's echoed state when present.
+    useReticulumVoiceStore.getState().setMicrophoneMuted(resp.microphone_muted ?? muted);
+  } catch (e) {
+    console.warn('[reticulumVoice] mute failed', e);
+    pushAppToast(i18n.t('reticulumVoice.errors.muteFailed'), 'error');
+  }
 }

Register reticulumVoice.errors.muteFailed in the English i18n keys. As per coding guidelines, "Stateful or I/O code must preserve integrity on failure and document failure points, fallbacks, and relevant logging."

🤖 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 `@src/renderer/lib/reticulumVoiceSession.ts` around lines 209 - 233, Update
reticulumVoiceAnswer, reticulumVoiceReject, reticulumVoiceHangup, and
reticulumVoiceSetMuted to catch rejected IPC calls, report failures through the
existing voice error toast/i18n patterns, and preserve local state integrity.
Keep clearCall() unconditional after reject/hangup attempts, and ensure
setMicrophoneMuted(muted) runs even when mute() fails; retain media teardown
before reject/hangup and register the reticulumVoice.errors.muteFailed English
i18n key.

Source: Coding guidelines

Comment thread src/renderer/runtime/useReticulumRuntime.voice.test.ts Outdated
Comment thread src/renderer/stores/reticulumVoiceStore.ts Outdated
Comment thread src/renderer/stores/reticulumVoiceStore.ts Outdated
@rinchen
rinchen marked this pull request as draft August 3, 2026 23:07
rinchen added 4 commits August 3, 2026 17:17
Keep hangup available during optimistic dial, improve identity resolution and LXST capability hints, add ringback/busy/fail tones with safety timeout, surface TX/RX stats, and log lifecycle-only call audits without per-packet noise.
Move PCM off the shared WS bus onto /ws/voice and reticulum:voiceAudio,
bound dest→identity cache growth, and fix incoming-call UI plus session
IPC/media races so answer, mute, and playback stay reliable.
Lock Probe-before-Path, shared cyan/slate class contracts, and Call/Send-file link shape so the Chat DM header cannot regress to mixed pills and bordered controls.
Flat cyan text-links did not read as clickable; shared action chrome now uses border/fill/hover so Peer details, Probe, Path, Call, and Send file match.
@rinchen rinchen changed the title feat(reticulum): integrate rsLXST voice calls feat(reticulum): rsLXST voice calls, Chat DM header polish, and dep updates Aug 3, 2026
rinchen added 3 commits August 3, 2026 18:31
Persist Default, Unscoped, and named scopes independently by viewKey so mesh-wide Public and metro-style channels do not bleed when switching pills; document RegionMesh region/scope guidance.
Stop silent outgoing audio and false fail toasts, gate terminal/media teardown by link/generation, and keep mute/reject/hangup optimistic UI in sync with IPC. Cover the paths with tests and refresh voice/flood-scope i18n plus docs.
Keep Flatpak standalone pnpm URLs/sha256s aligned with package.json packageManager after the pnpm bump.
@rinchen

rinchen commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 58 minutes.

rinchen added 2 commits August 3, 2026 20:39
Resolve remote_identity through the peer store so Calling/incoming UI shows a display name instead of a raw hash.
@rinchen

rinchen commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

rinchen added 10 commits August 3, 2026 21:13
Move the in-call mini-panel from the bottom-right corner to the viewport center so dialing is easier to notice.
Prefer hash-backed senders when own IDs are unknown, filter self from DM tabs, and derive own node ids from identity lxmf_hash earlier so autofocus cannot land on yourself.
…r mic thrash

Promote opportunistic LXMF and voice lifecycle lines to warn so developer bundles can classify inbound DM/call failures, and single-flight mic start per call generation on Answer.
Remember the last whisper peer so follow-up sends in the synthetic
whispers room go out as direct NOTICE instead of the join-room guard.

Fixes #788
…ress

Cold-start MQTT connected before deviceStore channel configs arrived, so
LongFast stayed on slot 0 (OnTrail). Re-push topic indices when resolved
configs change, and pre-arm MeshCore BLE MAC suppress from sticky storage
so Meshtastic NodeDB cannot revive the companion ghost before BLE attach.
Gate PCM TX until established and soft-drop early frames so lxst no longer
fatal-errors mid-dial and sticks the line busy. Progress tones and dialer
status follow connect → ring → in-call; toast only on connect failure and busy.
Switch ringback to a UK-style double ring with ~1s silence between pairs.
Treat unanswered outbound terminate (null/terminated) as connect-failed so
busy tone and toast always fire; harden safety hangup after silent clears.
Outbound calls play 2s dial, a peer-hash DTMF fingerprint, then UK ringback
(3s cycle). Connect-fail uses reorder; no-answer/busy use short dual busy;
toast on connect-fail, busy, no-answer, and unexpected drop.
Stop opening the mic on Answer before Established so Columba inbound
calls cannot fatal-error rsLXST with pre-establish PCM; gate renderer TX
the same way and cover Answer/TX/overlay/runtime paths with tests.
rinchen added 6 commits August 4, 2026 15:18
Derive the 4-digit burst from a full-hash XOR fold so peers no longer share
prefix-identical melodies, lengthen digits slightly, and wait 250ms before UK ring.
When AutoInterface is down or Direct fails on Auto, prefer live private
TCP/UDP hubs for LXMF recovery instead of hanging on a stuck Auto path.
…r pin

Wire prefer_private into LXMF rediscovery, correlate voice.error by link/generation,
pin RRC whisper reply targets against inbound overwrite, and backfill docs/i18n/tests.
Reconnect/connect contracts now assert commitConnectedMeshcoreBleSuppression
and preserveOrClearMeshcoreBleSuppression after the sticky-MAC helper extract.
Persist last-known LXMF hash to seed own-node ids before sidecar identity, and gate inferred DM tabs until own is known.
Adopt the radio-reported path.hash.mode into app settings instead of
pushing a stamped 1-byte default, and omit that key from AppPanel autosave
unless the user changes the dropdown.
@rinchen rinchen changed the title feat(reticulum): rsLXST voice calls, Chat DM header polish, and dep updates feat: rsLXST voice, MeshCore flood scopes, and multi-protocol reliability fixes Aug 4, 2026
@rinchen rinchen changed the title feat: rsLXST voice, MeshCore flood scopes, and multi-protocol reliability fixes feat: rsLXST voice, MeshCore flood scopes, security floors, and reliability fixes Aug 4, 2026
@rinchen
rinchen marked this pull request as ready for review August 4, 2026 23:00
@rinchen

rinchen commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped: 119 files exceed the limit of 100.

@rinchen
rinchen merged commit f39cbfd into main Aug 4, 2026
20 checks passed
@rinchen
rinchen deleted the update branch August 4, 2026 23:02
rinchen added a commit that referenced this pull request Aug 5, 2026
…ash allowlist (#793)

* feat(reticulum): modem handshake then carrier during voice connect

Replace post-DTMF auto-ringback with a short modem handshake and quiet
carrier bed; cut to UK ringback as soon as the call reaches connecting.

* fix(reticulum): allowlist reticulumLastSelfLxmfHash for appSettings:set

SQLite mirror of the last LXMF self hash was rejected by the IPC allowlist
since #785; localStorage still worked. Add the key and lock it in contracts.

* feat(reticulum): add encrypted LXMF paper message create and ingest

Wire sidecar paper APIs, deep-link/QR ingest, Chat share/scan UI, and Network Scan/import so encrypted paper messages can be exchanged without RF.

* fix(reticulum): harden LXMF paper persist, ingest, and i18n

Allow paper received_via through SQLite IPC, route create/ingest through shared LXMF helpers, map sidecar errors to stable codes, and fix locale scheme breakage check:i18n missed.

* fix(reticulum): address paper PR review findings

Gate paper UI on hasLxmfPaper, resolve paper-ingest sender names, restore modal focus, and harden i18n/tones/tooltip/tests from review.
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