Skip to content

Conversation

@yujonglee
Copy link
Contributor

No description provided.

@coderabbitai
Copy link

coderabbitai bot commented Sep 25, 2025

📝 Walkthrough

Walkthrough

This PR migrates local STT servers to an actor-based model (ractor), updates error handling for TranscribeService (String errors, test wrapper via HandleError), adds lifecycle/supervision/shutdown handling to the listener, restructures local-stt APIs and health checks via actors/registry, and updates dependencies (adds ractor, bumps backon).

Changes

Cohort / File(s) Summary
Dependency updates
Cargo.toml, plugins/listener/Cargo.toml, plugins/local-stt/Cargo.toml
Add ractor; bump backon 1.4.1 → 1.5.2; switch listener ractor dep to workspace; add async/networking deps (tokio, reqwest, futures-util, tracing, tokio-util) to local-stt.
Transcribe service error handling
crates/transcribe-whisper-local/src/service/streaming.rs, crates/transcribe-whisper-local/src/lib.rs
Change Service::Error from Infallible to String; tests wrap service with axum::error_handling::HandleError to map internal String errors to HTTP 500; minor test call flow adjustment.
Listener actor lifecycle
plugins/listener/src/actors/listener.rs
Add supervision handling; introduce shutdown channel in state; pre_start initializes Local STT and server; rx task listens for shutdown; handle_supervisor_evt reacts to failures; cleanup on stop.
Local STT public API surface
plugins/local-stt/src/lib.rs
Re-export server::*; remove internal_server/external_server fields from State.
Local STT extension refactor
plugins/local-stt/src/ext.rs
Replace SharedState-based management with ractor actors and registry; start/stop via Actor::spawn and stop_and_wait; health via GetHealth messages; introduce internal/external health helpers.
External STT actorization
plugins/local-stt/src/server/external.rs
Replace ServerHandle/run_server with ExternalSTTActor; define Args/State/Message; manage sidecar process, init retries (backon), health via messages, process event loop, and shutdown in post_stop.
Internal STT actorization
plugins/local-stt/src/server/internal.rs
Introduce InternalSTTActor with Args/State/Message; pre_start builds Whisper service, router, listener, and server task; GetHealth replies Ready; graceful shutdown in post_stop.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as App
  participant Listener as ListenerActor
  participant Registry as ractor::registry
  participant LSTT as Local STT Ext
  participant Int as InternalSTTActor
  participant Ext as ExternalSTTActor

  App->>Listener: start()
  Listener->>LSTT: start_server(...)
  LSTT->>Int: Actor::spawn(InternalSTTArgs)
  LSTT->>Ext: Actor::spawn(ExternalSTTArgs)
  LSTT->>Registry: register(Int, Ext)
  Note over Int,Ext: Actors initialize and become available
  Listener->>LSTT: get_servers()
  LSTT->>Int: GetHealth
  Int-->>LSTT: (base_url, Ready)
  LSTT->>Ext: GetHealth
  Ext-->>LSTT: (base_url, Health|Error)
  LSTT-->>Listener: Servers {internal, external}
  Note right of Listener: Processes audio events<br/>until shutdown
  App-->>Listener: stop (supervision/shutdown)
  Listener->>LSTT: stop_server()
  LSTT->>Registry: lookup(Int/Ext)
  LSTT->>Int: stop_and_wait
  LSTT->>Ext: stop_and_wait
  Int-->>LSTT: stopped
  Ext-->>LSTT: stopped
Loading
sequenceDiagram
  autonumber
  participant Test as Test (axum)
  participant Router as axum Router
  participant HE as HandleError
  participant TS as TranscribeService

  Test->>Router: request(from_realtime_audio)
  Router->>HE: call
  HE->>TS: call
  TS-->>HE: Result<..., Err(String)>
  HE-->>Router: Map Err(String) -> StatusCode 500
  Router-->>Test: HTTP 500 on error
Loading
sequenceDiagram
  autonumber
  participant Ext as ExternalSTTActor
  participant Sidecar as STT Sidecar Process
  participant Backon as backon::retry
  participant Client as hypr_am::Client

  Ext->>Sidecar: spawn()
  Ext->>Backon: retry InitRequest until OK
  Backon->>Client: init_request()
  Client-->>Backon: OK or Err
  Backon-->>Ext: init OK or retries exhausted
  Ext->>Client: status()
  Client-->>Ext: Health
  Note over Ext: handle(GetHealth)-> reply (base_url, health)
  Sidecar-->>Ext: Terminated/Error
  Ext->>Ext: handle(ProcessTerminated) -> update state
  Ext-->Sidecar: kill on post_stop
Loading
sequenceDiagram
  autonumber
  participant Int as InternalSTTActor
  participant Whisper as TranscribeService
  participant Http as axum server

  Int->>Whisper: build service/router (CORS)
  Int->>Http: bind & serve (graceful)
  Note over Int: handle(GetHealth) -> (base_url, Ready)
  Int->>Http: signal shutdown on post_stop
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

  • Deepgram compat v2 #1307 — Refactors local-stt to actor-based management and updates server API surfaces, overlapping with this PR’s Internal/External actorization and ext.rs changes.
  • Fix external stt server startups #1402 — Modifies local-stt server startup (run_server/ServerHandle); this PR replaces that model with actor lifecycles.
  • Refactor session actor #1485 — Alters listener actor supervision/registry behavior; this PR adds supervision handling and shutdown flow in listener.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning No pull request description was provided, so there is no contextual information relating to the extensive actor-based refactoring and dependency changes. This absence makes it impossible to understand the intent and scope of the changes without reviewing the diff. Please add a brief description summarizing the purpose and scope of the actor-based refactoring for the STT server, including key modules affected and high-level goals.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly captures the primary change of wrapping the STT server functionality with an actor-based architecture, matching the extensive modifications to internal and external STT server modules and related components. It is concise, specific, and accurately reflects the core transformation introduced in this pull request.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch wrap-stt-actor

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (3)
plugins/local-stt/src/ext.rs (2)

286-305: Propagate actor spawn errors instead of unwrapping.

Actor::spawn(...).await already returns a Result. If spawning fails, .unwrap() will bring down the whole process. Please propagate the error back to the caller.

For example:

-                let (server, _) = Actor::spawn(
+                let (server, _) = Actor::spawn(
                     Some(internal::InternalSTTActor::name()),
                     internal::InternalSTTActor,
                     internal::InternalSTTArgs {
                         model_cache_dir: cache_dir,
                         model_type: whisper_model,
                     },
-                )
-                .await
-                .unwrap();
+                )
+                .await?;

314-379: External server path has the same panic hazards.

Every .unwrap() on the spawn and health check can crash the app. Please mirror the internal path fix: propagate errors and use explicit durations.

plugins/local-stt/src/lib.rs (1)

18-28: Danger: State still referenced with removed fields.

State no longer carries internal_server/external_server, but ext::start_internal() and ext::start_external() still try to read/write those fields, leading to build failures (no field 'internal_server' on type 'State'). Please drop the obsolete mutations or re-home the handles before removing the fields.

-    state.internal_server = Some(handle.clone());
+    // store the handle wherever the new actor flow expects it,
+    // or remove this assignment entirely if no longer needed.
🧹 Nitpick comments (1)
plugins/local-stt/src/server/external.rs (1)

117-144: Drop the API key once initialization succeeds

We only need the API key during initialization. Leaving it in state.api_key keeps sensitive material around for the full actor lifetime and risks accidental logging or reuse.

Apply this diff to clear the key after a successful init:

         tracing::info!(res = ?res);
+        state.api_key = None;
         Ok(())
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 341cd33 and 490334b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • Cargo.toml (2 hunks)
  • crates/transcribe-whisper-local/src/lib.rs (3 hunks)
  • crates/transcribe-whisper-local/src/service/streaming.rs (1 hunks)
  • plugins/listener/Cargo.toml (1 hunks)
  • plugins/listener/src/actors/listener.rs (3 hunks)
  • plugins/local-stt/Cargo.toml (1 hunks)
  • plugins/local-stt/src/ext.rs (9 hunks)
  • plugins/local-stt/src/lib.rs (1 hunks)
  • plugins/local-stt/src/server/external.rs (1 hunks)
  • plugins/local-stt/src/server/internal.rs (1 hunks)
  • plugins/local-stt/src/server/mod.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit configuration file

**/*.{js,ts,tsx,rs}: 1. Do not add any error handling. Keep the existing one.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • plugins/local-stt/src/server/mod.rs
  • crates/transcribe-whisper-local/src/service/streaming.rs
  • crates/transcribe-whisper-local/src/lib.rs
  • plugins/local-stt/src/server/internal.rs
  • plugins/local-stt/src/server/external.rs
  • plugins/local-stt/src/lib.rs
  • plugins/local-stt/src/ext.rs
  • plugins/listener/src/actors/listener.rs
🧬 Code graph analysis (5)
crates/transcribe-whisper-local/src/lib.rs (1)
crates/transcribe-whisper-local/src/service/streaming.rs (2)
  • builder (33-35)
  • model_path (45-48)
plugins/local-stt/src/server/internal.rs (3)
plugins/listener/src/actors/listener.rs (5)
  • tokio (106-106)
  • name (35-37)
  • pre_start (45-53)
  • post_stop (55-62)
  • handle (64-76)
plugins/local-stt/src/server/external.rs (4)
  • name (33-35)
  • pre_start (43-111)
  • post_stop (146-166)
  • handle (168-196)
crates/transcribe-whisper-local/src/service/streaming.rs (2)
  • model_path (45-48)
  • builder (33-35)
plugins/local-stt/src/server/external.rs (4)
plugins/listener/src/actors/listener.rs (5)
  • tokio (106-106)
  • name (35-37)
  • pre_start (45-53)
  • post_stop (55-62)
  • handle (64-76)
plugins/local-stt/src/server/internal.rs (4)
  • name (33-35)
  • pre_start (43-93)
  • post_stop (95-104)
  • handle (106-124)
crates/am/src/client.rs (3)
  • new (11-16)
  • new (99-106)
  • status (25-34)
crates/host/src/lib.rs (1)
  • kill_processes_by_matcher (39-61)
plugins/local-stt/src/ext.rs (2)
plugins/local-stt/src/server/external.rs (1)
  • name (33-35)
plugins/local-stt/src/server/internal.rs (1)
  • name (33-35)
plugins/listener/src/actors/listener.rs (5)
plugins/local-stt/src/server/external.rs (1)
  • post_stop (146-166)
plugins/local-stt/src/server/internal.rs (1)
  • post_stop (95-104)
plugins/listener/src/actors/source.rs (1)
  • post_stop (175-188)
plugins/listener/src/actors/session.rs (2)
  • post_stop (241-272)
  • handle_supervisor_evt (191-239)
plugins/listener/src/actors/recorder.rs (1)
  • post_stop (153-168)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci (windows, windows-latest)
  • GitHub Check: ci (macos, macos-14)
🔇 Additional comments (8)
plugins/listener/Cargo.toml (1)

64-64: Workspace alignment looks good.

Switching ractor to the workspace dependency keeps this crate in sync with the shared version. 👍

crates/transcribe-whisper-local/src/service/streaming.rs (1)

65-67: Confirm downstream assumptions about the new error type.

Changing Service::Error to String works for the current handlers (they still always return Ok(Response)), but please double-check any call sites relying on Infallible. If any compile errors crop up, we’ll want to update those call sites to expect String.

crates/transcribe-whisper-local/src/lib.rs (1)

22-25: Wrapping the service with HandleError is the right move.

This keeps the tests aligned with the new String error surface and cleanly maps failures to HTTP 500.

plugins/listener/src/actors/listener.rs (1)

50-53: Monitor initialization needs error handling.

pg::monitor returns a Result. Right now the Err path is ignored; if monitoring fails we’ll proceed without supervision and never know. Please propagate or log the failure instead of dropping it.

You can bubble the error like this:

-        pg::monitor(tauri_plugin_local_stt::GROUP.into(), myself.get_cell());
+        pg::monitor(tauri_plugin_local_stt::GROUP.into(), myself.get_cell())
+            .map_err(|e| ActorProcessingErr::from(e))?;

Likely an incorrect or invalid review comment.

plugins/local-stt/src/server/external.rs (1)

50-53: Prevent panic when reserving port

port_check::free_local_port() returns None when it cannot find a free port (port exhaustion, race, permission issues). The current unwrap() turns that into a panic, tearing the actor down instead of letting supervision handle a recoverable failure.

Apply this diff to propagate the error instead of panicking:

-        let port = port_check::free_local_port().unwrap();
+        let port = port_check::free_local_port()
+            .ok_or_else(|| {
+                std::io::Error::new(
+                    std::io::ErrorKind::AddrNotAvailable,
+                    "no free port available",
+                )
+            })?;

Likely an incorrect or invalid review comment.

Cargo.toml (1)

132-142: Workspace dependency bump looks consistent.

Adding ractor = "0.15" and bumping backon to 1.5.2 at the workspace level lines up with the actor-based refactor described in the PR goals, and there’s nothing in the repo that would clash with those versions. 👍

plugins/local-stt/src/server/mod.rs (1)

4-4: Public GROUP constant works for actor grouping.

Exposing the "stt" GROUP constant here (and re-exporting it) gives the rest of the codebase a single source of truth for the actor group identifier—solid improvement.

plugins/local-stt/Cargo.toml (1)

77-83: Dependency additions align with the actor migration.

Bringing ractor, futures-util, tokio/tokio-util, tracing, and backon into the crate mirrors the new async actor flow; these are already workspace-managed, so it keeps versions consistent. Looks good.

@yujonglee yujonglee merged commit 0a93642 into main Sep 25, 2025
5 of 6 checks passed
@yujonglee yujonglee deleted the wrap-stt-actor branch September 25, 2025 06:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
plugins/local-stt/src/ext.rs (1)

556-562: Unwraps in background task can crash the task/thread.

calculate_file_checksum(...).unwrap() and remove_file(...).unwrap() can panic. Prefer handling failures (log and send -1) to avoid unexpected panics in the spawned task.

🧹 Nitpick comments (4)
plugins/local-stt/src/ext.rs (1)

475-488: Clamp and round download progress before casting to i8.

Casting raw f64 to i8 risks overflow/underflow and jitter. Clamp to [0, 100] after rounding.

-                DownloadProgress::Progress(downloaded, total_size) => {
-                    let percent = (downloaded as f64 / total_size as f64) * 100.0;
-                    let _ = channel.send(percent as i8);
-                }
+                DownloadProgress::Progress(downloaded, total_size) => {
+                    let pct = ((downloaded as f64 / total_size as f64) * 100.0).round();
+                    let pct = pct.clamp(0.0, 100.0) as i8;
+                    let _ = channel.send(pct);
+                }
plugins/listener/src/actors/listener.rs (2)

184-221: Avoid .unwrap() on event emission pipeline.

emit(...).unwrap() will panic on transient front-end disconnects. Return/log errors instead to keep the stream running.


242-258: Avoid .unwrap() on DB upsert.

db_upsert_session(...).await.unwrap() can panic and kill the task. Prefer returning/logging the error and continuing.

plugins/local-stt/src/server/external.rs (1)

48-52: Free-port probe is racy.

free_local_port().unwrap() followed by spawning a child to bind that port can race. Prefer letting the child choose the port or a retry loop on bind failure.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 490334b and d289294.

📒 Files selected for processing (5)
  • plugins/listener/src/actors/listener.rs (8 hunks)
  • plugins/local-stt/src/ext.rs (9 hunks)
  • plugins/local-stt/src/lib.rs (1 hunks)
  • plugins/local-stt/src/server/external.rs (1 hunks)
  • plugins/local-stt/src/server/internal.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit configuration file

**/*.{js,ts,tsx,rs}: 1. Do not add any error handling. Keep the existing one.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • plugins/local-stt/src/lib.rs
  • plugins/local-stt/src/ext.rs
  • plugins/local-stt/src/server/internal.rs
  • plugins/local-stt/src/server/external.rs
  • plugins/listener/src/actors/listener.rs
🧬 Code graph analysis (4)
plugins/local-stt/src/ext.rs (2)
plugins/local-stt/src/server/external.rs (1)
  • name (33-35)
plugins/local-stt/src/server/internal.rs (1)
  • name (33-35)
plugins/local-stt/src/server/internal.rs (2)
plugins/listener/src/actors/listener.rs (6)
  • tokio (122-122)
  • tokio (123-123)
  • name (36-38)
  • pre_start (46-64)
  • post_stop (66-76)
  • handle (78-90)
plugins/local-stt/src/server/external.rs (4)
  • name (33-35)
  • pre_start (43-99)
  • post_stop (134-152)
  • handle (154-185)
plugins/local-stt/src/server/external.rs (4)
plugins/local-stt/src/server/internal.rs (4)
  • name (33-35)
  • pre_start (43-91)
  • post_stop (93-101)
  • handle (103-121)
crates/am/src/client.rs (3)
  • new (11-16)
  • new (99-106)
  • status (25-34)
plugins/local-stt/src/ext.rs (8)
  • state (155-155)
  • state (294-294)
  • state (463-463)
  • state (525-525)
  • state (566-566)
  • state (585-585)
  • models_dir (23-23)
  • models_dir (74-76)
crates/host/src/lib.rs (1)
  • kill_processes_by_matcher (39-61)
plugins/listener/src/actors/listener.rs (3)
plugins/local-stt/src/server/internal.rs (2)
  • post_stop (93-101)
  • handle (103-121)
plugins/listener/src/actors/session.rs (3)
  • post_stop (241-272)
  • handle_supervisor_evt (191-239)
  • handle (118-189)
crates/ws/src/client.rs (1)
  • finalize_with_text (23-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci (windows, windows-latest)
  • GitHub Check: ci (macos, macos-14)
🔇 Additional comments (6)
plugins/local-stt/src/lib.rs (1)

18-18: Re-export widens public API surface; confirm intent and semver impact.

pub use server::*; exposes internal actor types/messages publicly. Ensure this is intentional and compatible with your semver guarantees.

plugins/local-stt/src/ext.rs (2)

361-397: Good: coordinated shutdown via stop_and_wait.

Using stop_and_wait and handling the Result ensures clean, awaited shutdowns. This addresses prior races.


629-647: Use Duration for call_t! timeout and avoid unwraps (already raised before).

Switch numeric 10 * 1000 to an explicit Duration. Also prefer returning the error upward instead of mapping to None. This was flagged previously.

-async fn internal_health() -> Option<(String, ServerHealth)> {
+async fn internal_health() -> Option<(String, ServerHealth)> {
+    use std::time::Duration;
     match registry::where_is(internal::InternalSTTActor::name()) {
         Some(cell) => {
             let actor: ActorRef<internal::InternalSTTMessage> = cell.into();
-            match call_t!(actor, internal::InternalSTTMessage::GetHealth, 10 * 1000) {
+            match call_t!(actor, internal::InternalSTTMessage::GetHealth, Duration::from_secs(10)) {
                 Ok(r) => Some(r),
                 Err(_) => None,
             }
         }
         None => None,
     }
 }
 
-async fn external_health() -> Option<(String, ServerHealth)> {
+async fn external_health() -> Option<(String, ServerHealth)> {
+    use std::time::Duration;
     match registry::where_is(external::ExternalSTTActor::name()) {
         Some(cell) => {
             let actor: ActorRef<external::ExternalSTTMessage> = cell.into();
-            match call_t!(actor, external::ExternalSTTMessage::GetHealth, 10 * 1000) {
+            match call_t!(actor, external::ExternalSTTMessage::GetHealth, Duration::from_secs(10)) {
                 Ok(r) => Some(r),
                 Err(_) => None,
             }
         }
         None => None,
     }
 }
plugins/listener/src/actors/listener.rs (2)

66-76: Graceful shutdown path looks good.

Sending the shutdown signal before aborting the task increases chances of a clean teardown.


92-106: Stopping on any child failure is aggressive (previously flagged).

Terminating the listener on any ActorFailed removes recovery paths. Consider a bounded retry/supervision strategy instead.

plugins/local-stt/src/server/internal.rs (1)

119-120: Keep actor alive on request-level errors (previously flagged).

Turning ServerError into Err(...) tears the actor down on a single failed request.

-            InternalSTTMessage::ServerError(e) => Err(e.into()),
+            InternalSTTMessage::ServerError(e) => {
+                tracing::error!("internal STT request failed: {}", e);
+                Ok(())
+            }

Comment on lines +56 to +64
let (tx, rx_task, shutdown_tx) = spawn_rx_task(args, myself).await.unwrap();
let state = ListenerState {
tx,
rx_task,
shutdown_tx: Some(shutdown_tx),
};

Ok(state)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t unwrap spawn_rx_task in pre_start.

.await.unwrap() will panic on setup failure. Propagate with ? so the actor fails to start cleanly.

-        let (tx, rx_task, shutdown_tx) = spawn_rx_task(args, myself).await.unwrap();
+        let (tx, rx_task, shutdown_tx) = spawn_rx_task(args, myself).await?;
📝 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
let (tx, rx_task, shutdown_tx) = spawn_rx_task(args, myself).await.unwrap();
let state = ListenerState {
tx,
rx_task,
shutdown_tx: Some(shutdown_tx),
};
Ok(state)
}
let (tx, rx_task, shutdown_tx) = spawn_rx_task(args, myself).await?;
let state = ListenerState {
tx,
rx_task,
shutdown_tx: Some(shutdown_tx),
};
Ok(state)
}
🤖 Prompt for AI Agents
In plugins/listener/src/actors/listener.rs around lines 56 to 64, the call to
spawn_rx_task uses `.await.unwrap()` which will panic on failure; replace the
unwrap with `?` to propagate the error, update the enclosing function's return
type to a Result (if not already) and adjust any error conversion (e.g., using
.map_err or Into) so the spawn_rx_task error can be returned to the actor
system; ensure shutdown_tx is wrapped the same way after using `?` so the
ListenerState construction remains unchanged.

Comment on lines +267 to +276
let (_server, _) = Actor::spawn(
Some(internal::InternalSTTActor::name()),
internal::InternalSTTActor,
internal::InternalSTTArgs {
model_cache_dir: cache_dir,
model_type: whisper_model,
},
)
.await
.unwrap();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Avoid panicking on actor spawn failures.

Actor::spawn(...).await.unwrap() will crash the app if spawn fails. Propagate the error instead of unwrapping.

🤖 Prompt for AI Agents
In plugins/local-stt/src/ext.rs around lines 267 to 276, the code currently
calls Actor::spawn(...).await.unwrap(), which will panic on spawn failure;
change this to propagate the error instead of unwrapping by using the ? operator
(or map the spawn error into the function's error type and return Err) so the
caller can handle failures. Ensure the enclosing function returns a Result (or
compatible error type), propagate the Actor::spawn().await error through that
Result, and add any necessary conversions/mapping to match the function's error
type.

Comment on lines +278 to 279
let base_url = internal_health().await.map(|r| r.0).unwrap();
Ok(base_url)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Health lookup unwrap can race and panic.

internal_health().await.map(|r| r.0).unwrap() will panic if the actor isn’t ready within the call_t! timeout. Return a proper error or retry rather than unwrapping.

🤖 Prompt for AI Agents
In plugins/local-stt/src/ext.rs around lines 278-279, the call
internal_health().await.map(|r| r.0).unwrap() can panic if the actor isn’t
ready; replace the unwrap with proper error handling by awaiting
internal_health(), matching on the Result (or using map_err/and_then) to return
a descriptive Err instead of panicking, or implement a short retry/backoff loop
with a timeout and then return an error if still unavailable—ensure the function
returns an appropriate error variant with context rather than calling unwrap.

Comment on lines +331 to +342
let (_server, _) = Actor::spawn(
Some(external::ExternalSTTActor::name()),
external::ExternalSTTActor,
external::ExternalSTTArgs {
cmd,
api_key: am_key,
model: am_model,
models_dir: data_dir,
},
)
.await
.unwrap();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

External actor spawn unwrap is unsafe.

Same as internal: Actor::spawn(...).await.unwrap() will bring down the process on failure.

🤖 Prompt for AI Agents
In plugins/local-stt/src/ext.rs around lines 331 to 342, the call
Actor::spawn(...).await.unwrap() will abort the process on failure; replace the
unwrap with proper error handling: call .await and either propagate the error
with the ? operator (adding a suitable return Result type) or match the Result
and log the failure with context (using the crate's logger or anyhow::Context)
and return an Err/early return. Ensure you preserve any cleanup or state and
include a clear contextual message when returning or logging the spawn error.

Comment on lines +344 to 346
let base_url = external_health().await.map(|v| v.0).unwrap();
Ok(base_url)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

External health unwrap can panic.

external_health().await.map(|v| v.0).unwrap() can panic if health fails. Avoid unwrap here as well.

🤖 Prompt for AI Agents
In plugins/local-stt/src/ext.rs around lines 344 to 346, the call
external_health().await.map(|v| v.0).unwrap() can panic if the health check
fails; replace the unwrap with proper error handling by propagating the failure
or returning a descriptive error instead. Specifically, await external_health(),
map or destructure its Ok variant to extract v.0, and return Err or use the ?
operator on the Result so the function returns an appropriate error instead of
panicking; ensure the error you return/log includes context that this is an
external health resolution failure.

Comment on lines +118 to +128
.retry(
ConstantBuilder::default()
.with_max_times(20)
.with_delay(std::time::Duration::from_millis(500)),
)
.when(|e| {
tracing::error!("external_stt_init_failed: {:?}", e);
true
})
.sleep(tokio::time::sleep)
.await?;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Retry budget likely too small for model init.

20 attempts x 500ms = ~10s. Model init can take much longer. Increase budget and consider exponential backoff to avoid premature failure.

-        .retry(
-            ConstantBuilder::default()
-                .with_max_times(20)
-                .with_delay(std::time::Duration::from_millis(500)),
-        )
+        .retry(
+            ConstantBuilder::default()
+                .with_max_times(120) // ~60s at 500ms; tune per model size
+                .with_delay(std::time::Duration::from_millis(500)),
+        )
📝 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
.retry(
ConstantBuilder::default()
.with_max_times(20)
.with_delay(std::time::Duration::from_millis(500)),
)
.when(|e| {
tracing::error!("external_stt_init_failed: {:?}", e);
true
})
.sleep(tokio::time::sleep)
.await?;
.retry(
ConstantBuilder::default()
.with_max_times(120) // ~60s at 500ms; tune per model size
.with_delay(std::time::Duration::from_millis(500)),
)
.when(|e| {
tracing::error!("external_stt_init_failed: {:?}", e);
true
})
.sleep(tokio::time::sleep)
.await?;
🤖 Prompt for AI Agents
In plugins/local-stt/src/server/external.rs around lines 118 to 128, the retry
budget is only 20 attempts with a fixed 500ms delay (~10s) which is likely too
short for model initialization; change the retry strategy to allow a much larger
total wait (e.g. increase max attempts or total duration) and switch to
exponential backoff with jitter rather than a constant 500ms delay to avoid
premature failure and thundering retries; update the retry builder to use an
exponential/backoff policy (or increase delay progressively), raise max attempts
or permit a longer max elapsed time (e.g. minutes), and keep the tracing::error
logging inside the closure so failures are still logged while backing off.

Comment on lines +178 to 180
if let Err(e) = reply_port.send((state.base_url.clone(), status)) {
return Err(e.into());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t kill the actor when reply_port is closed.

If the caller drops the RPC channel (timeout/cancel), sending will fail and you return Err, terminating the actor. Treat send failure as benign.

-                if let Err(e) = reply_port.send((state.base_url.clone(), status)) {
-                    return Err(e.into());
-                }
+                let _ = reply_port.send((state.base_url.clone(), status));
📝 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
if let Err(e) = reply_port.send((state.base_url.clone(), status)) {
return Err(e.into());
}
// Don’t fail the actor if the reply port has been closed.
- if let Err(e) = reply_port.send((state.base_url.clone(), status)) {
- return Err(e.into());
let _ = reply_port.send((state.base_url.clone(), status));
🤖 Prompt for AI Agents
In plugins/local-stt/src/server/external.rs around lines 178-180, the current
code returns Err when reply_port.send fails, which kills the actor if the RPC
channel was dropped; change this to treat send failures as benign: instead of
returning Err on send error, swallow it (e.g. match or if let Err(e) =
reply_port.send(...) { log/debug the error with context } ) and continue normal
execution so the actor is not terminated when the receiver has been closed.

Comment on lines +77 to +85
let server_task = tokio::spawn(async move {
axum::serve(listener, router)
.with_graceful_shutdown(async move {
shutdown_rx.changed().await.ok();
})
.await
.unwrap();
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Server task .unwrap() can crash the actor thread.

If axum::serve(...).await errors, this .unwrap() will panic. Handle the error or log it without panicking.

-                .await
-                .unwrap();
+                .await
+                .unwrap_or_else(|e| tracing::error!("internal_stt_serve_error: {}", e));
📝 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
let server_task = tokio::spawn(async move {
axum::serve(listener, router)
.with_graceful_shutdown(async move {
shutdown_rx.changed().await.ok();
})
.await
.unwrap();
});
let server_task = tokio::spawn(async move {
axum::serve(listener, router)
.with_graceful_shutdown(async move {
shutdown_rx.changed().await.ok();
})
.await
.unwrap_or_else(|e| tracing::error!("internal_stt_serve_error: {}", e));
});
🤖 Prompt for AI Agents
In plugins/local-stt/src/server/internal.rs around lines 77 to 85, the call to
axum::serve(...).await currently uses .unwrap() which will panic the actor
thread on error; replace the unwrap with proper error handling by awaiting the
serve result into a variable or matching on it and logging the error (e.g., if
let Err(e) = result { error!("HTTP server exited with error: {}", e); }) or
returning/propagating the error instead of panicking, ensuring the task exits
cleanly and does not bring down the whole actor.

Comment on lines +113 to +115
if let Err(e) = reply_port.send((state.base_url.clone(), status)) {
return Err(e.into());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not tear down the actor on reply_port send failure.

If the caller timed out and dropped the port, reply_port.send fails and the actor returns Err, causing termination. Treat it as a no-op.

-                if let Err(e) = reply_port.send((state.base_url.clone(), status)) {
-                    return Err(e.into());
-                }
+                let _ = reply_port.send((state.base_url.clone(), status));
📝 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
if let Err(e) = reply_port.send((state.base_url.clone(), status)) {
return Err(e.into());
}
let _ = reply_port.send((state.base_url.clone(), status));
🤖 Prompt for AI Agents
In plugins/local-stt/src/server/internal.rs around lines 113 to 115, the code
currently returns Err when reply_port.send fails which causes the actor to
terminate if the caller dropped the port; change the error handling so that send
failures are treated as a no-op: catch the Err(e) from reply_port.send,
optionally log/debug the failure, but do not return Err or propagate it — simply
continue execution so the actor is not torn down.

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.

2 participants