Skip to content

Commit b857a22

Browse files
fix: address PR #474 review comments — 3 fixes in Flutter voice-agent bridge
- **QEF-1 (coderabbit, P0/Major, line 318)**: Pass `handle.address` across `Isolate.run` boundary instead of the raw `Pointer<Void>` and reconstruct it via `RacHandle.fromAddress(...)` inside `_processVoiceTurnInIsolate`. Matches the pattern already used by `dart_bridge_tts`, `dart_bridge_stt`, `dart_bridge_llm`, and `dart_bridge_vlm`; passing Dart-side pointers across isolate boundaries is unsafe. - **QEF-3 (greptile, P2, line 109)**: Add a clarifying comment explaining why `_initFuture` is cleared *after* `completer.complete(_handle!)` — concurrent callers already holding a reference to `completer.future` still receive the value normally; new callers arriving after this line hit the `_handle != null` fast path. - **QEF-4 (greptile outside-diff)**: Replace the plain `String` type on `VoiceAgentModelLoadedEvent.component` with a new typed enum `VoiceAgentComponent { stt, llm, tts }`, and update the three emit sites (`loadSTTModel`, `loadLLMModel`, `loadTTSVoice`) accordingly. Per the project's style guide ("never use strings directly"), the enum makes component typos a compile-time error rather than a silently-accepted string. Not addressed: - **QEF-2 (greptile, P2 — `sttResultFree` should be nullable)**: Already resolved on this branch via a different approach — `sttResultFree` was removed from `native_functions.dart` entirely. The STT transcription path runs inside `Isolate.run(...)` which cannot read main-isolate static state, so each spawned isolate resolves `rac_stt_result_free` inline. The comment block at `native_functions.dart:89-93` explicitly documents this decision. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a0c25aa commit b857a22

1 file changed

Lines changed: 23 additions & 7 deletions

File tree

sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_voice_agent.dart

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ class DartBridgeVoiceAgent {
106106
_handle = handlePtr.value;
107107
_logger.info('Voice agent created with shared component handles');
108108
completer.complete(_handle!);
109+
// Clear _initFuture after completing the completer so that concurrent
110+
// callers that already hold a reference to completer.future receive the
111+
// value normally. New callers arriving after this line hit the
112+
// `_handle != null` fast path.
109113
_initFuture = null;
110114
return _handle!;
111115
} finally {
@@ -214,7 +218,8 @@ class DartBridgeVoiceAgent {
214218
}
215219

216220
_logger.info('Voice agent STT model loaded: $modelId');
217-
_eventController.add(const VoiceAgentModelLoadedEvent(component: 'stt'));
221+
_eventController.add(const VoiceAgentModelLoadedEvent(
222+
component: VoiceAgentComponent.stt));
218223
} finally {
219224
calloc.free(pathPtr);
220225
calloc.free(idPtr);
@@ -239,7 +244,8 @@ class DartBridgeVoiceAgent {
239244
}
240245

241246
_logger.info('Voice agent LLM model loaded: $modelId');
242-
_eventController.add(const VoiceAgentModelLoadedEvent(component: 'llm'));
247+
_eventController.add(const VoiceAgentModelLoadedEvent(
248+
component: VoiceAgentComponent.llm));
243249
} finally {
244250
calloc.free(pathPtr);
245251
calloc.free(idPtr);
@@ -264,7 +270,8 @@ class DartBridgeVoiceAgent {
264270
}
265271

266272
_logger.info('Voice agent TTS voice loaded: $voiceId');
267-
_eventController.add(const VoiceAgentModelLoadedEvent(component: 'tts'));
273+
_eventController.add(const VoiceAgentModelLoadedEvent(
274+
component: VoiceAgentComponent.tts));
268275
} finally {
269276
calloc.free(pathPtr);
270277
calloc.free(idPtr);
@@ -314,16 +321,22 @@ class DartBridgeVoiceAgent {
314321
'Voice agent not ready. Load models and initialize first.');
315322
}
316323

317-
// Run the heavy C++ processing in a background isolate
318-
return Isolate.run(() => _processVoiceTurnInIsolate(handle, audioData));
324+
// Capture handle address before entering isolate — passing the raw Pointer
325+
// across an isolate boundary is unsafe; pass the address and reconstruct it.
326+
final handleAddress = handle.address;
327+
return Isolate.run(
328+
() => _processVoiceTurnInIsolate(handleAddress, audioData));
319329
}
320330

321331
/// Static helper for processing voice turn in an isolate.
322332
/// The C++ API expects raw audio bytes (PCM16), not float samples.
333+
/// Must be static/top-level for Isolate.run().
323334
static Future<VoiceTurnResult> _processVoiceTurnInIsolate(
324-
RacHandle handle,
335+
int handleAddress,
325336
Uint8List audioData,
326337
) async {
338+
final handle = RacHandle.fromAddress(handleAddress);
339+
327340
// Allocate native memory for audio data (raw PCM16 bytes)
328341
final audioPtr = calloc<Uint8>(audioData.length);
329342
final resultPtr = calloc<RacVoiceAgentResultStruct>();
@@ -555,9 +568,12 @@ class VoiceAgentInitializedEvent extends VoiceAgentEvent {
555568
const VoiceAgentInitializedEvent();
556569
}
557570

571+
/// Component types that can emit a model-loaded event on the voice agent.
572+
enum VoiceAgentComponent { stt, llm, tts }
573+
558574
/// Voice agent model loaded.
559575
class VoiceAgentModelLoadedEvent extends VoiceAgentEvent {
560-
final String component; // 'stt', 'llm', or 'tts'
576+
final VoiceAgentComponent component;
561577
const VoiceAgentModelLoadedEvent({required this.component});
562578
}
563579

0 commit comments

Comments
 (0)