Skip to content

v2 SDK testing-pass fixes: telemetry, Flutter, React Native, Web#511

Open
Siddhesh2377 wants to merge 13 commits into
mainfrom
v2-sdk-testing-fixes
Open

v2 SDK testing-pass fixes: telemetry, Flutter, React Native, Web#511
Siddhesh2377 wants to merge 13 commits into
mainfrom
v2-sdk-testing-fixes

Conversation

@Siddhesh2377

@Siddhesh2377 Siddhesh2377 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

v2 SDK testing-pass fixes across telemetry, Flutter, React Native, and Web.

Telemetry (commons + per-SDK)

  • Attach telemetry sink in Phase 1 (fixes dropped system/device events); emit LLM telemetry from tool-calling turns; propagate baseURL to the model-assignment bridge.
  • Populate V2 fields across modalities via the proto properties-carrier (no proto regen): embeddings dimension/total tokens, RAG top_k/retrieval_time, LoRA base_model_id/adapter_id, VLM token metrics, framework + session_id; env gate; VAD/voice/download telemetry.
  • Report OS platform (not "flutter"/"react_native"); publish embeddings events from the lifecycle path.

Flutter

  • Offload blocking inference to worker isolates; wire the voice-agent mic driver; run turns off the UI isolate; fix telemetry auth-token resolution and the download size-gate.

React Native

  • Example app v2 redesign: Android-style composer, LLM streaming, IME padding, icon fixes, LoRA model tag + telemetry.

Web

  • Export rac_auth_get_access_token for WASM telemetry auth.
  • Fix model discovery (WASM framework exports + models.hydrated UI refresh), live download progress (async http_download slot + event-driven C++ driver), VLM pthread-pool deadlock, RAG + Vision download UI, prebuilt ONNX/sherpa WASM vendoring.

Known follow-ups (not in this branch)

  • Web ONNX local-build instantiation (large-module wasmTable export under emcc) — blocks RAG embeddings + speech end-to-end.
  • Web VLM main-thread freeze — worker-offload scaffolded but not wired.

Note

Medium Risk
Touches SDK environment and backend auth across three platforms; the React Native example commits a default production-style API key in source, which is a credential exposure risk for a public repo.

Overview
Aligns Android, Flutter, and React Native example apps with staging RunAnywhere.initialize so local/dev Railway base URLs and telemetry/logging work (production was ignoring custom URLs and silencing logs).

Flutter adds --dart-define defaults for API key/URL, lazy tab mounting so hidden tabs don’t spin up view-models, and guards against model-lifecycle feedback loops (chat + voice) that could ANR. RAG/VLM/voice get pipeline/camera/dispose cleanup fixes; Android Gradle/Kotlin bumps and small launcher/status-bar polish.

React Native is a large v2 example redesign: flat RootNavigator (3 tabs + feature routes), themed IntroScreen, redesigned chat composer/messages/model picker on action-sheet bottom sheets, reanimated keyboard padding, Figtree fonts, and Genie removed from the example Android build path. Bootstrap now falls back to in-repo default Railway API key/URL when Settings has no custom config (staging init). Android example pins kotlinx-coroutines for AAR compatibility.

Reviewed by Cursor Bugbot for commit 2bbba92. Configure here.

Summary by CodeRabbit

  • New Features

    • Added a unified theme/design system across React Native, plus improved chat/vision/voice UI components.
    • Expanded vision to support loading images from disk (instead of camera-only).
    • Enhanced voice agent flow with VAD-aware readiness and more robust turn handling.
    • Improved model download experience with clearer progress/state, plus richer telemetry.
  • Bug Fixes

    • Prevented orphaned RAG pipelines on ingest failures and improved pipeline cleanup on teardown.
    • Tightened model lifecycle syncing to avoid redundant refresh loops.
    • Improved telemetry delivery and expanded captured metrics.
  • Chores

    • Switched examples to use staging configuration and updated build tooling.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refreshes example apps across Android, Flutter, React Native, and web, updates React Native navigation and themed UI, expands SDK telemetry and download instrumentation, and adds Flutter bridge isolate-based native calls plus a voice-agent mic driver.

Changes

Android, Flutter, and web example updates

Layer / File(s) Summary
Android and Flutter setup changes
examples/android/RunAnywhereAI/app/build.gradle.kts, examples/android/RunAnywhereAI/app/src/main/java/.../RunAnywhereApplication.kt, examples/flutter/RunAnywhereAI/.gitignore, examples/flutter/RunAnywhereAI/android/..., examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart, examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart
Android and Flutter examples switch staging-oriented SDK setup, add default config environment constants, update Android resources and Gradle versions, and ignore local dart_defines.json.
Flutter runtime and lifecycle fixes
examples/flutter/RunAnywhereAI/lib/app/content_view.dart, examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart, .../rag/rag_view_model.dart, .../vision/vlm_camera_view.dart, .../voice/*
Flutter app tabs are lazily built, model lifecycle listeners filter redundant updates, and RAG, vision, and voice cleanup paths are tightened.
Flutter example logging and text cleanup
examples/flutter/RunAnywhereAI/lib/core/services/model_catalog_bootstrap.dart, .../features/benchmarks/*, .../features/models/*, .../features/solutions/solutions_view.dart, .../features/vision/vlm_view_model.dart
Flutter example log strings and benchmark comments are normalized without changing the underlying model and solution flows.
Web model and vision interactions
examples/web/RunAnywhereAI/src/components/model-selection.ts, src/main.ts, src/views/documents.ts, src/views/vision.ts
Web examples subscribe once to hydrated model events, add document-model download controls, and support loading an image file for VLM analysis.

React Native example restructure

Layer / File(s) Summary
Build, assets, and app bootstrap
examples/react-native/RunAnywhereAI/App.tsx, android/*, ios/*, metro.config.js, package.json, react-native.config.js, index.js
The React Native example removes Genie gating, adds native-lib sync and asset linking, updates dependencies, bundles fonts, and replaces app bootstrap with staged initialization plus intro and root navigator rendering.
Theme system and navigation foundation
examples/react-native/RunAnywhereAI/src/theme/system/*, src/navigation/*, src/features/intro/IntroScreen.tsx, src/types/index.ts
A shared theme system, typed routes, new stack and tab navigators, transitions, and intro screen replace the prior navigation and styling foundation.
Chat and sheet UI primitives
examples/react-native/RunAnywhereAI/src/components/chat/*, src/components/common/ModelRequiredOverlay.tsx, src/components/model/ModelSelectionSheet.tsx, src/components/ui/BottomSheet.tsx, src/features/chat/components/*
Shared chat UI is rebuilt around bottom sheets, themed controls, streaming indicators, prompt suggestions, header actions, and model selection grouped by on-device versus available models.
Chat experience flow
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx, src/screens/ConversationListScreen.tsx
Chat screen wiring persists tool-calling state, marks streaming assistant messages, uses the new header and suggestion components, and moves conversation and LoRA management into sheets.
Feature screen rewrites
examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx, RAGScreen.tsx, STTScreen.tsx, SolutionsScreen.tsx, TTSScreen.tsx, VADScreen.tsx, VisionHubScreen.tsx, VoiceAssistantScreen.tsx
Feature screens are reworked with the theme system and updated model, recording, document, and session flows across RAG, STT, TTS, VAD, voice assistant, solutions, and vision areas.
Benchmark and settings screens
examples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsx, SettingsScreen.tsx
Benchmark and settings screens switch to themed layouts, richer progress and history views, explicit model download iteration, and refactored API and tool-calling configuration handling.

Commons telemetry and download instrumentation

Layer / File(s) Summary
Telemetry contracts and payload schema
sdk/runanywhere-commons/include/rac/infrastructure/telemetry/*
Telemetry headers add queued HTTP wakeup and polling APIs and expand payload structs with VLM, RAG, embeddings, LoRA, image generation, VAD, and voice-turn fields.
Telemetry transport and translation
sdk/runanywhere-commons/src/infrastructure/network/environment.cpp, .../telemetry/telemetry_json.cpp, .../telemetry/telemetry_manager.cpp
Telemetry sending now applies to non-development environments, JSON serialization emits more fields, and the manager translates richer event properties while supporting queued HTTP delivery.
Feature telemetry emitters
sdk/runanywhere-commons/src/core/events.cpp, src/features/{diffusion,embeddings,llm,lora,rag,stt,tts,vad,vlm,voice_agent}/*
Feature modules publish richer started, completed, failed, and per-turn telemetry with framework, token, retrieval, adapter, image, and voice metrics.
Download orchestration and device events
sdk/runanywhere-commons/src/infrastructure/device/rac_device_manager.cpp, src/infrastructure/download/download_orchestrator.cpp
Device registration emits a registered event on repeat launches, and download orchestration adds once-only event emission plus an async web download driver path.

Flutter plugin transport and bridge updates

Layer / File(s) Summary
Android transport and shared bridge wiring
sdk/runanywhere-flutter/packages/runanywhere/android/src/main/kotlin/.../OkHttpHttpTransport.kt, sdk/runanywhere-flutter/packages/runanywhere/lib/native/{dart_bridge.dart,dart_bridge_auth.dart,dart_bridge_device.dart}
Flutter transport avoids blocking OkHttp calls on the Android main thread, passes base URL into model assignment registration, and adjusts auth and device registration behavior.
Async native bridge methods
sdk/runanywhere-flutter/packages/runanywhere/lib/native/{dart_bridge_embeddings.dart,dart_bridge_llm.dart,dart_bridge_rag.dart,dart_bridge_stt.dart}
Flutter bridge methods add isolate-based async native calls for embeddings, RAG ingest and query, STT lifecycle transcription, and LLM stream worker startup.
Telemetry wakeup bridge
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart
Flutter telemetry switches from callback-per-request delivery to wakeup-driven queue draining with phase-1 sink attachment and adapter-based HTTP sending.
Voice agent mic driver
sdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dart
A new mic driver segments PCM audio into turns, streams voice-agent events, collects synthesized audio, and plays TTS replies while suppressing self-transcription.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • RunanywhereAI/runanywhere-sdks#204: The main PR’s React Native example refactors existing UI and components, which directly builds on the retrieved PR that introduced those same components and screens.
  • RunanywhereAI/runanywhere-sdks#251: The main PR’s Flutter example changes to lib/app/runanywhere_ai_app.dart directly overlap with the retrieved PR’s corresponding SDK wiring and initialization refactor in the same file.
  • RunanywhereAI/runanywhere-sdks#471: The main PR’s voice UI/state changes now require and track VAD model readiness, which is directly enabled by the retrieved PR’s VAD model loading infrastructure.

Suggested reviewers

  • sanchitmonga22
  • shubhammalhotra28

Poem

A rabbit tapped the build with care,
and found new paths in code laid bare.
The tabs now wake when first they’re seen,
telemetry hums behind the screen.
With sheets and streams and icons bright,
this burrow ships a busier night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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
Title check ✅ Passed The title 'v2 SDK testing-pass fixes: telemetry, Flutter, React Native, Web' clearly summarizes the main change—addressing testing-pass issues across multiple SDK platforms and modalities.
Description check ✅ Passed The PR description is comprehensive, covering telemetry improvements, Flutter/React Native/Web SDK changes, and known follow-ups. It includes a detailed summary matching the template structure with Type of Change and implementation details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v2-sdk-testing-fixes

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Committed API key in source
    • Removed the hardcoded React Native example API key and now only uses backend credentials saved in Settings.
  • ✅ Fixed: RAG UI stale after reload fail
    • Reset the loaded document, conversation, question, and thinking state when document loading fails after pipeline teardown.

Create PR

Or push these changes by commenting:

@cursor push 620c1a3f76
Preview (620c1a3f76)
diff --git a/examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart b/examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
--- a/examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
+++ b/examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
@@ -97,6 +97,12 @@
       _llmSupportsThinking = llmModel.supportsThinking;
     } catch (e) {
       _error = e.toString();
+      _documentName = null;
+      _isDocumentLoaded = false;
+      _messages = [];
+      _currentQuestion = '';
+      _lastResult = null;
+      _llmSupportsThinking = false;
       // Tear down any partially-created pipeline so a failed ingest doesn't
       // leave an orphaned C++ pipeline session behind.
       await RunAnywhere.rag.destroyPipeline();

diff --git a/examples/react-native/RunAnywhereAI/App.tsx b/examples/react-native/RunAnywhereAI/App.tsx
--- a/examples/react-native/RunAnywhereAI/App.tsx
+++ b/examples/react-native/RunAnywhereAI/App.tsx
@@ -86,13 +86,6 @@
 } from './src/screens/SettingsScreen';
 import { logDiagnostic } from './src/utils/diagnostics';
 
-// Default backend config for the development Railway backend (mirrors the
-// Android example's gitignored local.properties → BuildConfig.RUNANYWHERE_*).
-// Used when no custom configuration has been saved in Settings.
-const DEFAULT_BASE_URL =
-  'https://runanywhere-backend-development.up.railway.app';
-const DEFAULT_API_KEY = 'runa_prod_f_sf4FrG4LhuUia4vupidcuIx5fD0t-usz2eNqKBeQw';
-
 type InitState = 'loading' | 'ready' | 'error';
 
 const InitializationErrorView: React.FC<{
@@ -163,9 +156,9 @@
       const hasCustomConfig = await hasCustomConfiguration();
 
       const effectiveApiKey =
-        hasCustomConfig && customApiKey ? customApiKey : DEFAULT_API_KEY;
+        hasCustomConfig && customApiKey ? customApiKey : '';
       const effectiveBaseURL =
-        hasCustomConfig && customBaseURL ? customBaseURL : DEFAULT_BASE_URL;
+        hasCustomConfig && customBaseURL ? customBaseURL : '';
 
       if (
         hasUsableBackendConfig({

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 2bbba92. Configure here.

// Used when no custom configuration has been saved in Settings.
const DEFAULT_BASE_URL =
'https://runanywhere-backend-development.up.railway.app';
const DEFAULT_API_KEY = 'runa_prod_f_sf4FrG4LhuUia4vupidcuIx5fD0t-usz2eNqKBeQw';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Committed API key in source

High Severity

The React Native example now embeds a full DEFAULT_API_KEY string in App.tsx and uses it whenever Settings has no custom key. That exposes a production-style credential in git history and lets any clone call the Railway backend without local secrets, unlike the Android/Flutter examples that keep keys out of the repo.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2bbba92. Configure here.

_error = e.toString();
// Tear down any partially-created pipeline so a failed ingest doesn't
// leave an orphaned C++ pipeline session behind.
await RunAnywhere.rag.destroyPipeline();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

RAG UI stale after reload fail

Medium Severity

When loadDocument fails after a document was already loaded, the new catch calls destroyPipeline() but leaves _isDocumentLoaded and related UI fields from the previous success. The screen still looks loaded while the native pipeline is gone, so queries fail until the user clears state manually.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2bbba92. Configure here.

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

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (9)
examples/react-native/RunAnywhereAI/App.tsx (2)

159-209: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace console.log calls with allowed logging APIs.

Lines 176/185/192/206 use console.log, which violates the React Native example logging policy.

As per coding guidelines, “Remove console.log statements; only console.warn and console.error are allowed.”

🤖 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 `@examples/react-native/RunAnywhereAI/App.tsx` around lines 159 - 209, Replace
all four console.log calls in the initialization block with console.warn or
console.error as appropriate per coding guidelines. Specifically, change the
console.log for '[App] Found backend configuration', '[App] SDK initialized with
backend configuration (staging)', '[App] SDK initialized in DEVELOPMENT mode',
and the final SDK initialization details log to use console.warn for
informational messages (or console.error if these represent error conditions).
Only console.warn and console.error are permitted; console.log is not allowed.

Source: Coding guidelines


122-142: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reintroduce optional Genie backend registration in App init.

Lines 122-142 now register only LlamaCPP and ONNX, so Genie backend availability is never probed/registered in App.tsx.

🧩 Suggested direction
 async function registerBackends(): Promise<BackendRegistrationState> {
   const llamaResult = LlamaCPP ? await LlamaCPP.register() : false;
   const llamaRegistered = llamaResult !== false;
+  const genieResult = Genie ? await Genie.register() : false;
+  const genieRegistered = genieResult !== false;
   const onnxResult = ONNX ? await ONNX.register() : false;
   const onnxRegistered = onnxResult !== false;
-  return { llamaRegistered, onnxRegistered };
+  return { llamaRegistered, genieRegistered, onnxRegistered };
 }

As per coding guidelines, “Use dynamic require() with try/catch for optional backend registration (LlamaCPP, Genie, ONNX) in App.tsx initialization flow” and “Filter Genie NPU backend models per Qualcomm chip ID … Android platform only.”

🤖 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 `@examples/react-native/RunAnywhereAI/App.tsx` around lines 122 - 142, The
registerBackends() function is currently missing Genie backend registration. Add
Genie backend registration following the same pattern as LlamaCPP and ONNX
backends, using dynamic require() with try/catch for optional backend loading.
Include logic to filter Genie NPU backend models based on Qualcomm chip ID for
Android platform only, similar to how llamaRegistered and onnxRegistered are
determined. Add appropriate diagnostic logging for Genie backend registration
status and include the Genie registration state in the returned
BackendRegistrationState object.

Source: Coding guidelines

examples/react-native/RunAnywhereAI/react-native.config.js (1)

11-19: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing autolinking disables for New Architecture compatibility.

Per the coding guidelines, this config must disable autolinking for react-native-live-audio-stream (iOS only), react-native-audio-recorder-player, react-native-sound, and react-native-tts to prevent New Architecture/Nitrogen incompatibility issues. As per coding guidelines, "Disable autolinking in react-native.config.js for: react-native-live-audio-stream (iOS), react-native-audio-recorder-player (both), react-native-sound (both), react-native-tts (both) due to New Architecture/Nitrogen incompatibility".

🔧 Proposed fix to add required autolinking disables
   dependencies: {
     // Nitro modules requires Turbo codegen for iOS (NitroModulesSpec.h)
     'react-native-nitro-modules': {
       platforms: {
         android: null,
         ios: {},
       },
     },
+    // Disable autolinking for packages incompatible with New Architecture/Nitrogen
+    'react-native-live-audio-stream': {
+      platforms: {
+        ios: null,
+      },
+    },
+    'react-native-audio-recorder-player': {
+      platforms: {
+        android: null,
+        ios: null,
+      },
+    },
+    'react-native-sound': {
+      platforms: {
+        android: null,
+        ios: null,
+      },
+    },
+    'react-native-tts': {
+      platforms: {
+        android: null,
+        ios: null,
+      },
+    },
   },
🤖 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 `@examples/react-native/RunAnywhereAI/react-native.config.js` around lines 11 -
19, The dependencies object in react-native.config.js is missing required
autolinking disables for packages incompatible with New Architecture/Nitrogen.
Add entries in the dependencies object for react-native-live-audio-stream (with
ios disabled), react-native-audio-recorder-player (with both android and ios
disabled), react-native-sound (with both android and ios disabled), and
react-native-tts (with both android and ios disabled), following the same
pattern as the existing react-native-nitro-modules configuration where disabled
platforms are set to null and enabled ones to an empty object.

Source: Coding guidelines

examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx (1)

531-535: ⚠️ Potential issue | 🟠 Major

Use manual async iteration instead of aggregateStream in ChatScreen.

Line 534-539 uses aggregateStream() which is the streaming helper approach. Per the ChatScreen coding guideline, you must use manual async iteration (via Symbol.asyncIterator()) for generateStream() on Android due to Hermes incompatibility with NitroModules custom async iterables.

Reference the pattern used in VLMService.ts and VoiceAssistantScreen.tsx:

const iter = stream[Symbol.asyncIterator]();
let result = await iter.next();
while (!result.done) {
  const event = result.value;
  // handle event
  result = await iter.next();
}
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx` around lines
531 - 535, Replace the call to RunAnywhere.aggregateStream() with manual async
iteration using Symbol.asyncIterator() to ensure Hermes compatibility on
Android. After getting the eventStream from RunAnywhere.generateStream(),
instead of passing it to aggregateStream(), manually iterate through the stream
by calling Symbol.asyncIterator() on eventStream, then repeatedly call next() on
the iterator in a while loop until done is true, handling each event value as it
arrives. Follow the exact pattern demonstrated in VLMService.ts and
VoiceAssistantScreen.tsx where the iterator is created, result checked for done
status, and the event is extracted from result.value before calling next()
again.

Source: Coding guidelines

examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx (1)

350-363: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Live mode drops earlier finalized phrases.

At Line 360/Line 361, each finalized segment replaces the prior transcript instead of appending to it, so only the latest phrase remains visible.

Suggested patch
-          if (partial.isFinal || finalText) {
-            accumulatedTranscriptRef.current = text;
-            setTranscript(text);
+          if (partial.isFinal || finalText) {
+            const combined = accumulatedTranscriptRef.current
+              ? `${accumulatedTranscriptRef.current} ${text}`
+              : text;
+            accumulatedTranscriptRef.current = combined;
+            setTranscript(combined);
             setPartialTranscript('');
             setConfidence(partial.finalOutput?.confidence ?? null);
           } else {
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx` around lines
350 - 363, In the consumeLiveTranscription function, the issue is that when a
finalized segment is received (in the if block checking partial.isFinal or
finalText), the code replaces the entire transcript with just the new text
instead of appending to it. To fix this, modify the lines that set
accumulatedTranscriptRef.current and setTranscript so that they append the new
finalized text to the existing transcript value rather than replacing it
completely. This will preserve all previously finalized phrases while adding the
new one.
examples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsx (1)

118-124: ⚠️ Potential issue | 🟠 Major

Replace for await...of with manual iterator consumption for SDK streams.

Line 122 uses for await...of with warmupEvents (from RunAnywhere.generateStream()), which violates the requirement to use manual async iteration for SDK async iterables due to Hermes incompatibility with NitroModules custom async iterables.

Suggested patch
-    for await (const event of warmupEvents) {
-      if (event.isFinal) break;
-    }
+    const warmupIterator = warmupEvents[Symbol.asyncIterator]();
+    try {
+      let step = await warmupIterator.next();
+      while (!step.done) {
+        if (step.value.isFinal) break;
+        step = await warmupIterator.next();
+      }
+    } finally {
+      await warmupIterator.return?.();
+    }
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsx` around
lines 118 - 124, The for await...of loop consuming the warmupEvents stream from
RunAnywhere.generateStream() needs to be replaced with manual async iterator
consumption to avoid Hermes incompatibility issues with NitroModules custom
async iterables. Replace the for await...of construct with a while loop that
manually calls the next() method on the iterator obtained from
warmupEvents[Symbol.asyncIterator](), checking the done property in each
iteration and accessing the value property to check event.isFinal until the
condition is met.

Source: Coding guidelines

examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx (1)

183-192: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clean up the VAD stream if recording startup fails.

After isListeningRef.current = true, any rejection from startRecording leaves the stream open and the VAD consumer waiting while the UI still says it is not listening.

Proposed fix
     const stream = createPushableAudioStream();
     streamRef.current = stream;
     isListeningRef.current = true;
     setLatestResult(null);
     setFrameCount(0);
-    taskRef.current = consumeVAD(RunAnywhere.streamVAD(stream.iterable));
-    await capture.startRecording((chunk) => {
-      stream.push(pcm16ChunkToFloat32Bytes(chunk));
-    });
-    setIsListening(true);
+    try {
+      taskRef.current = consumeVAD(RunAnywhere.streamVAD(stream.iterable));
+      await capture.startRecording((chunk) => {
+        stream.push(pcm16ChunkToFloat32Bytes(chunk));
+      });
+      setIsListening(true);
+    } catch (error) {
+      isListeningRef.current = false;
+      stream.close();
+      await taskRef.current?.catch(() => {});
+      streamRef.current = null;
+      taskRef.current = null;
+      Alert.alert('Listen Failed', `Failed to start VAD: ${error}`);
+    }
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx` around lines
183 - 192, The issue is that if capture.startRecording() fails after
isListeningRef.current is set to true, the stream created by
createPushableAudioStream() remains open and the VAD consumer task in
taskRef.current is left waiting, creating a resource leak. Wrap the
capture.startRecording() call in a try-catch block and in the catch handler,
clean up by closing the stream stored in streamRef.current, cleaning up the VAD
task in taskRef.current, resetting isListeningRef.current back to false, and
then re-throwing the error so the UI properly reflects the failure state.
examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx (1)

453-459: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the new console.log diagnostics.

These changed blocks add console.log calls in the React Native example. Keep diagnostics as console.warn/console.error only when actionable, or remove them before merge. As per coding guidelines, examples/react-native/RunAnywhereAI/**/*.{ts,tsx,js,jsx}: Remove console.log statements; only console.warn and console.error are allowed.

Also applies to: 474-480, 595-602

🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx` around
lines 453 - 459, The console.log statement logging the generation settings (with
temperature, maxTokens, systemPrompt, and thinkingModeEnabled properties)
violates the React Native example coding guidelines which only allow
console.warn and console.error. Remove this entire console.log call along with
the other console.log calls mentioned at lines 474-480 and 595-602 in the same
file, keeping only console.warn or console.error if there are actionable
diagnostics that need to be preserved.

Source: Coding guidelines

sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart (1)

203-205: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Destroy Phase-1 telemetry managers even when Phase 2 never initialized.

attachSinkPhase1() creates _managerPtr without setting _isInitialized; if initialization is reset or fails before Phase 2, this guard returns early and leaves the C++ telemetry sink/manager attached.

🧹 Proposed lifecycle fix
   /// Shutdown telemetry manager
   static void shutdown() {
-    if (!_isInitialized || _managerPtr == null) return;
+    final managerPtr = _managerPtr;
+    if (managerPtr == null) {
+      _isInitialized = false;
+      return;
+    }

     try {
       final lib = PlatformLoader.loadCommons();
@@
       final destroy = lib.lookupFunction<Void Function(Pointer<Void>),
           void Function(Pointer<Void>)>('rac_telemetry_manager_destroy');

-      destroy(_managerPtr!);
+      destroy(managerPtr);
       _managerPtr = null;
       _isInitialized = false;
       _logger.debug('Telemetry manager shutdown');

Also applies to: 214-222

🤖 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
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart`
around lines 203 - 205, The shutdown() method guards against cleanup using both
_isInitialized and _managerPtr == null conditions, but since attachSinkPhase1()
creates _managerPtr without setting _isInitialized, the early return prevents
Phase-1 managers from being destroyed when Phase-2 initialization fails. Fix the
guard condition in shutdown() to only check if _managerPtr == null is true,
removing the _isInitialized check, so that any created manager pointers are
properly destroyed regardless of whether full initialization completed.
🟡 Minor comments (14)
examples/web/RunAnywhereAI/src/components/model-selection.ts-111-117 (1)

111-117: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Snapshot listeners before dispatch to avoid skipped callbacks.

Line 111 iterates listeners directly. If a listener unsubscribes itself (or another listener) during execution, splice mutates the array and later listeners can be skipped in the same cycle.

💡 Proposed fix
-        for (const listener of listeners) {
+        for (const listener of [...listeners]) {
           try {
             listener();
           } catch (err) {
             console.warn('[model-selection] hydrated listener threw', err);
           }
         }
🤖 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 `@examples/web/RunAnywhereAI/src/components/model-selection.ts` around lines
111 - 117, The for loop iterating over the listeners array can skip callbacks
because mutations to the listeners array (via splice when listeners unsubscribe)
cause the iteration to become misaligned. Create a snapshot of the listeners
array before the for loop that iterates over listener() calls, so that
unsubscribe operations during iteration do not affect which listeners are
executed in the current cycle.
examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart-179-183 (1)

179-183: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard fire-and-forget teardown errors in dispose().

Line 182 starts an unawaited teardown with no error handler. If it fails, it can surface as an uncaught async exception during widget disposal.

Suggested fix
-    unawaited(RunAnywhere.rag.destroyPipeline());
+    unawaited(
+      RunAnywhere.rag.destroyPipeline().catchError((Object e, StackTrace st) {
+        debugPrint('RAG destroyPipeline in dispose failed: $e');
+      }),
+    );
🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart` around
lines 179 - 183, The dispose() method in RagViewModel has a fire-and-forget
async call to RunAnywhere.rag.destroyPipeline() without error handling, which
can cause uncaught exceptions during widget disposal. Add error handling to the
unawaited call by attaching a catch handler or using .onError to gracefully
handle any exceptions that occur during the pipeline destruction. This ensures
failures during teardown do not surface as uncaught async exceptions when the
widget is disposed.
examples/react-native/RunAnywhereAI/src/navigation/RootNavigator.tsx-36-67 (1)

36-67: ⚠️ Potential issue | 🟡 Minor

Modal routes are declared but not registered in the navigation stack.

Lines 36-67 register tabs and feature screens, but ModelSelection, Lora, ConversationList, and ChatAnalytics are declared in routes.ts and navigation.types.ts without being registered as Stack.Screen components. While ChatScreen currently renders these as modal components using local state rather than navigation, this creates a mismatch between the declared navigation contracts and the actual implementation. If any code attempts to navigate to these routes (e.g., navigate('ModelSelection')), React Navigation will not find a matching screen. Either register these as Stack.Screen components or remove them from the shared navigation contracts.

🤖 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 `@examples/react-native/RunAnywhereAI/src/navigation/RootNavigator.tsx` around
lines 36 - 67, The modal routes ModelSelection, Lora, ConversationList, and
ChatAnalytics are declared in routes.ts and navigation.types.ts but are missing
as Stack.Screen components in the RootNavigator Stack.Navigator. Either add
these four routes as Stack.Screen components within the Stack.Navigator (similar
to how VisionHubScreen, STTScreen, and other screens are registered), providing
the appropriate component and options for each, or if these routes are only
meant for local state-based modal rendering in ChatScreen, remove them from the
shared navigation contracts in routes.ts and navigation.types.ts to eliminate
the mismatch between declared and implemented routes.
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx-280-285 (1)

280-285: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Close the model sheet only after a successful load.

Line 284 closes the sheet unconditionally, but loadModel (Lines 293-349) swallows failures and returns void, so failed loads still dismiss the picker.

🐛 Suggested change
-  const handleModelSelected = useCallback(async (model: SDKModelInfo) => {
+  const handleModelSelected = useCallback(async (model: SDKModelInfo) => {
     // The sheet shows its own Loading spinner during the await; once the model
     // is loaded we close it so the user lands directly in the chat.
-    await loadModel(model);
-    setShowModelSelection(false);
-  }, []);
+    const loaded = await loadModel(model);
+    if (loaded) setShowModelSelection(false);
+  }, [loadModel]);

-  const loadModel = async (model: SDKModelInfo) => {
+  const loadModel = async (model: SDKModelInfo): Promise<boolean> => {
     try {
       setIsModelLoading(true);
...
       if (!model.isDownloaded && !model.localPath) {
         Alert.alert(
           'Error',
           'Model has not been downloaded. Open the model picker to download it first.'
         );
-        return;
+        return false;
       }
...
       if (result.success) {
...
         const tools = await RunAnywhere.getRegisteredTools();
         setRegisteredToolCount(tools.length);
+        return true;
       } else {
...
+        return false;
       }
     } catch (error) {
       console.error('[ChatScreen] Error loading model:', error);
       Alert.alert('Error', `Failed to load model: ${error}`);
+      return false;
     } finally {
       setIsModelLoading(false);
     }
   };

Also applies to: 293-349

🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx` around lines
280 - 285, The handleModelSelected callback unconditionally closes the model
selection sheet by calling setShowModelSelection(false) after awaiting
loadModel, but since loadModel swallows failures and returns void, the sheet
closes even when the model fails to load. Modify the loadModel function to
indicate success or failure (either by returning a boolean success value or by
throwing an error on failure), and update handleModelSelected to only call
setShowModelSelection(false) when loadModel completes successfully. If loadModel
throws an error, catch it and keep the sheet open so the user can try selecting
another model.
examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx-88-90 (1)

88-90: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add accessibility metadata to icon-only buttons.

The new-chat, clear-search, and delete controls are icon-only touch targets and should expose role/label/hint so screen readers can operate them reliably.

Proposed fix
-        <TouchableOpacity onPress={handleCreate} hitSlop={8}>
+        <TouchableOpacity
+          onPress={handleCreate}
+          hitSlop={8}
+          accessibilityRole="button"
+          accessibilityLabel="Start new conversation"
+        >
...
-          <TouchableOpacity onPress={() => setSearchQuery('')} hitSlop={8}>
+          <TouchableOpacity
+            onPress={() => setSearchQuery('')}
+            hitSlop={8}
+            accessibilityRole="button"
+            accessibilityLabel="Clear search"
+          >
...
-                  <TouchableOpacity
+                  <TouchableOpacity
                     style={styles.deleteBtn}
                     onPress={() => handleDelete(c)}
                     hitSlop={8}
+                    accessibilityRole="button"
+                    accessibilityLabel={`Delete conversation ${c.title}`}
                   >

Also applies to: 107-109, 184-190

🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx`
around lines 88 - 90, The icon-only button components (TouchableOpacity elements
with Icon children for new-chat, clear-search, and delete controls) lack
accessibility metadata needed for screen readers. Add accessibility properties
to each of these TouchableOpacity components: set accessible to true, add
accessibilityRole as "button", provide a descriptive accessibilityLabel for each
button's purpose (e.g., "Create new chat", "Clear search", "Delete"), and
optionally add accessibilityHint with additional context. Apply these changes to
all three icon-only button instances mentioned: the new-chat button with
handleCreate handler, the clear-search button around lines 107-109, and the
delete button around lines 184-190.
examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx-44-48 (1)

44-48: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trimmed search text is not actually used for filtering.

trim() is only used for the truthy check; the actual query passed to searchConversations(...) still includes leading/trailing whitespace, which can produce false “No matches”.

Proposed fix
-  const filtered = useMemo(
-    () =>
-      searchQuery.trim() ? searchConversations(searchQuery) : conversations,
-    [conversations, searchQuery, searchConversations]
-  );
+  const filtered = useMemo(() => {
+    const query = searchQuery.trim();
+    return query ? searchConversations(query) : conversations;
+  }, [conversations, searchQuery, searchConversations]);
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx`
around lines 44 - 48, The searchQuery.trim() is only used for the boolean check
in the useMemo hook, but the untrimmed searchQuery is still passed to
searchConversations function, causing searches with leading/trailing whitespace
to not work as expected. Store the trimmed query in a variable (e.g., const
trimmedQuery = searchQuery.trim()) and use that trimmedQuery variable both in
the conditional check and as the argument passed to searchConversations to
ensure whitespace is consistently removed before filtering.
examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx-113-113 (1)

113-113: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep conversation taps responsive while the keyboard is open.

Without keyboardShouldPersistTaps, the first tap on a row often just dismisses the keyboard instead of selecting the conversation.

Proposed fix
-      <BottomSheetScrollView contentContainerStyle={styles.list}>
+      <BottomSheetScrollView
+        contentContainerStyle={styles.list}
+        keyboardShouldPersistTaps="handled"
+      >
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx`
at line 113, Add the `keyboardShouldPersistTaps` prop to the
`BottomSheetScrollView` component in ConversationListScreen to ensure that taps
on conversation rows register properly even when the keyboard is open. Set this
prop to "handled" or "always" (depending on your preference for when keyboard
dismissal should be allowed) so that the first tap on a conversation row
actually selects it instead of just dismissing the keyboard.
sdk/runanywhere-commons/src/infrastructure/network/environment.cpp-61-66 (1)

61-66: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix contradictory telemetry-gate comment.

The comment says telemetry is sent in every environment, but Line 66 excludes development. Please align the wording with the actual condition to avoid future gate regressions.

🤖 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 `@sdk/runanywhere-commons/src/infrastructure/network/environment.cpp` around
lines 61 - 66, The multi-line comment describing the telemetry gate behavior
contradicts the actual return condition. The comment states "telemetry is sent
in every environment" but the return statement on the line with `env !=
RAC_ENV_DEVELOPMENT` explicitly excludes development from the gate. Update the
comment to accurately clarify that this function determines whether telemetry
requires the normal send gate with authentication (true for staging and
production), while development is intentionally excluded because it uses a
separate immediate-flush path to the local backend. This will prevent future
confusion about what the gate controls and why development is excluded.
sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp-328-338 (1)

328-338: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Emit zero-valued inference steps consistently.

The comment says steps=0 is meaningful, but Line 336 still uses add_int, which drops 0. Use the always-emitting helper like seed so imagegen rows do not lose that metric.

Proposed fix
-        json.add_int("num_inference_steps", payload->num_inference_steps);
+        json.add_int_always("num_inference_steps", payload->num_inference_steps);
🤖 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 `@sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp`
around lines 328 - 338, The num_inference_steps field on line 336 currently uses
add_int which will drop zero values, but according to the comment zero values
are meaningful for this metric. Change the add_int call for num_inference_steps
to use add_int_always instead, consistent with how the seed field is handled on
line 337, so that zero-valued inference steps are properly emitted in the
telemetry data.
examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx-121-126 (1)

121-126: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset LoRA adapter state when the selected model changes while hidden.

Because refresh() only runs when visible is true, onAdaptersChanged([]) does not fire when modelId changes while the sheet is closed, so the ChatScreen LoRA badge can keep showing the previous model’s adapter count.

Proposed state reset
+  useEffect(() => {
+    setAvailableAdapters([]);
+    updateLoaded([]);
+    setError(null);
+  }, [modelId, updateLoaded]);
+
   // Only refresh once the user actually opens the sheet — the sheet is mounted
   // (hidden) on the chat screen at startup, so an unconditional mount-refresh
   // would hit the LoRA service before any model is loaded.
   useEffect(() => {
-    if (visible) refresh();
+    if (visible) {
+      void refresh();
+    }
   }, [visible, refresh]);
🤖 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 `@examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx` around
lines 121 - 126, The useEffect hook in LoRASheet.tsx only refreshes when the
sheet becomes visible, but this means the LoRA adapter state is not reset when
the selected model changes while the sheet is hidden. Add modelId to the
dependency array of the useEffect hook, and update the effect logic to call
onAdaptersChanged([]) to clear the adapter state whenever modelId changes while
the sheet is not visible, ensuring the ChatScreen LoRA badge accurately reflects
the current model's adapter count.
examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx-132-144 (1)

132-144: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an accessibility label to the icon-only send/stop button.

The primary composer action currently renders only an icon or stop square, so assistive tech can announce it as an unlabeled button.

Proposed accessibility update
         <TouchableOpacity
           style={[
             styles.action,
             {
               backgroundColor: canPressAction
                 ? colors.primary
                 : colors.surfaceVariant,
             },
           ]}
           onPress={handleAction}
           disabled={!canPressAction}
           activeOpacity={0.8}
+          accessibilityRole="button"
+          accessibilityLabel={canStop ? 'Stop generating' : 'Send message'}
+          accessibilityState={{ disabled: !canPressAction }}
         >
🤖 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 `@examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx` around
lines 132 - 144, The TouchableOpacity component for the send/stop button lacks
an accessibility label, preventing screen readers from announcing its purpose to
users. Add an accessibilityLabel prop to the TouchableOpacity component that
dynamically provides a descriptive label based on the button's current state
(e.g., "Send message" when canPressAction is true, and "Stop" when false). This
will ensure assistive technologies can properly announce the button's function
to users relying on accessibility features.
examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx-1084-1089 (1)

1084-1089: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wire the Documentation row or remove it.

The row is tappable but its onPress body is empty, so users get no feedback when tapping Documentation.

Do you want me to wire this to the project docs URL or open an issue to track the missing destination?

🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx` around
lines 1084 - 1089, The TouchableOpacity component wrapping the Documentation
Text in SettingsScreen has an empty onPress handler that provides no
functionality when tapped. Either implement the onPress callback to open the
project documentation URL (using a method like Linking.openURL or navigation),
or remove the entire TouchableOpacity and Documentation Text row if the
documentation link is not needed for this screen.
examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx-96-106 (1)

96-106: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Bound the activity log in state, not only at render time.

activityLog.slice(0, 20) limits display, but every speech transition is still retained in memory for long listening sessions.

Proposed fix
       setActivityLog((prev) => [
         { type: 'started', timestampMs: Date.now() },
         ...prev,
-      ]);
+      ].slice(0, 20));
     } else if (!speechDetected && prevSpeechRef.current) {
       setActivityLog((prev) => [
         { type: 'ended', timestampMs: Date.now() },
         ...prev,
-      ]);
+      ].slice(0, 20));

Also applies to: 409-409

🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx` around lines
96 - 106, The activity log state is unbounded and grows indefinitely with every
speech transition event, even though it is only sliced to 20 items at display
time. In the useEffect hook where setActivityLog is called for both speech
'started' and 'ended' events, apply bounds directly to the state update by
limiting the array size when creating the new log entry (for example, using
slice to keep only the most recent entries in the prev array before prepending
the new entry). This ensures the memory footprint remains constant during long
listening sessions, rather than relying solely on display-time slicing.
sdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dart-105-109 (1)

105-109: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Forward recorder stream errors to subscribers.

The mic stream onError only logs, leaving events open and silent after a runtime capture failure. Surface the error and stop the driver so the UI can exit the listening state.

🐛 Proposed fix
     _micSub = stream.listen(
       _onChunk,
-      onError: (Object e, StackTrace st) => _logger.warning('Mic error: $e'),
+      onError: (Object e, StackTrace st) {
+        _logger.warning('Mic error: $e');
+        if (!_out.isClosed) {
+          _out.addError(e, st);
+        }
+        unawaited(stop());
+      },
       cancelOnError: false,
     );
🤖 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
`@sdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dart`
around lines 105 - 109, The onError handler in the _micSub stream listener only
logs the error but does not notify subscribers or stop the driver, leaving the
voice agent in a broken state. Modify the onError callback to forward the error
to the events sink so subscribers are notified of the failure, and call the
appropriate cleanup or stop method to halt the driver. This allows the UI to
detect the error and exit the listening state properly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f6ab52c-ef30-4833-8ba9-a1d9f2b1e4ff

📥 Commits

Reviewing files that changed from the base of the PR and between c272a1f and 2bbba92.

⛔ Files ignored due to path filters (26)
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • examples/flutter/RunAnywhereAI/assets/fonts/figtree.ttf is excluded by !**/*.ttf
  • examples/flutter/RunAnywhereAI/assets/fonts/figtree_italic.ttf is excluded by !**/*.ttf
  • examples/flutter/RunAnywhereAI/assets/fonts/maple_mono.ttf is excluded by !**/*.ttf
  • examples/flutter/RunAnywhereAI/lib/generated/solutions_yaml.dart is excluded by !**/generated/**
  • examples/react-native/RunAnywhereAI/android/app/src/main/assets/fonts/Figtree-Italic.ttf is excluded by !**/*.ttf
  • examples/react-native/RunAnywhereAI/android/app/src/main/assets/fonts/Figtree.ttf is excluded by !**/*.ttf
  • examples/react-native/RunAnywhereAI/android/app/src/main/assets/fonts/MapleMono.ttf is excluded by !**/*.ttf
  • examples/react-native/RunAnywhereAI/assets/fonts/Figtree-Italic.ttf is excluded by !**/*.ttf
  • examples/react-native/RunAnywhereAI/assets/fonts/Figtree.ttf is excluded by !**/*.ttf
  • examples/react-native/RunAnywhereAI/assets/fonts/MapleMono.ttf is excluded by !**/*.ttf
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (134)
  • examples/android/RunAnywhereAI/app/build.gradle.kts
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt
  • examples/flutter/RunAnywhereAI/.gitignore
  • examples/flutter/RunAnywhereAI/android/app/src/main/AndroidManifest.xml
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/drawable/ic_launcher_foreground.xml
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/values-night/styles.xml
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/values/styles.xml
  • examples/flutter/RunAnywhereAI/android/build.gradle
  • examples/flutter/RunAnywhereAI/android/gradle/wrapper/gradle-wrapper.properties
  • examples/flutter/RunAnywhereAI/android/settings.gradle
  • examples/flutter/RunAnywhereAI/lib/app/content_view.dart
  • examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart
  • examples/flutter/RunAnywhereAI/lib/core/services/model_catalog_bootstrap.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/llm_benchmark_provider.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/tts_benchmark_provider.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_list_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart
  • examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/solutions/solutions_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_agent_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_component_view_model_base.dart
  • examples/react-native/RunAnywhereAI/App.tsx
  • examples/react-native/RunAnywhereAI/android/app/build.gradle
  • examples/react-native/RunAnywhereAI/android/gradle.properties.example
  • examples/react-native/RunAnywhereAI/android/link-assets-manifest.json
  • examples/react-native/RunAnywhereAI/android/settings.gradle
  • examples/react-native/RunAnywhereAI/babel.config.js
  • examples/react-native/RunAnywhereAI/index.js
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxproj
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI/Info.plist
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAITests/Info.plist
  • examples/react-native/RunAnywhereAI/ios/link-assets-manifest.json
  • examples/react-native/RunAnywhereAI/metro.config.js
  • examples/react-native/RunAnywhereAI/package.json
  • examples/react-native/RunAnywhereAI/react-native.config.js
  • examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx
  • examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx
  • examples/react-native/RunAnywhereAI/src/components/chat/MessageBubble.tsx
  • examples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsx
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/components/ui/BottomSheet.tsx
  • examples/react-native/RunAnywhereAI/src/features/chat/components/ChatHeader.tsx
  • examples/react-native/RunAnywhereAI/src/features/chat/components/PromptSuggestions.tsx
  • examples/react-native/RunAnywhereAI/src/features/intro/IntroScreen.tsx
  • examples/react-native/RunAnywhereAI/src/navigation/BottomTabs.tsx
  • examples/react-native/RunAnywhereAI/src/navigation/RootNavigator.tsx
  • examples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsx
  • examples/react-native/RunAnywhereAI/src/navigation/navigation.types.ts
  • examples/react-native/RunAnywhereAI/src/navigation/routes.ts
  • examples/react-native/RunAnywhereAI/src/navigation/transitions.ts
  • examples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ConversationListScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/RAGScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SolutionsScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VisionHubScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx
  • examples/react-native/RunAnywhereAI/src/services/ModelCatalogBootstrap.ts
  • examples/react-native/RunAnywhereAI/src/theme/system/ThemeProvider.tsx
  • examples/react-native/RunAnywhereAI/src/theme/system/colors.ts
  • examples/react-native/RunAnywhereAI/src/theme/system/dimens.ts
  • examples/react-native/RunAnywhereAI/src/theme/system/icons.tsx
  • examples/react-native/RunAnywhereAI/src/theme/system/index.ts
  • examples/react-native/RunAnywhereAI/src/theme/system/motion.ts
  • examples/react-native/RunAnywhereAI/src/theme/system/typography.ts
  • examples/react-native/RunAnywhereAI/src/types/index.ts
  • examples/web/RunAnywhereAI/src/components/model-selection.ts
  • examples/web/RunAnywhereAI/src/main.ts
  • examples/web/RunAnywhereAI/src/views/documents.ts
  • examples/web/RunAnywhereAI/src/views/vision.ts
  • package.json
  • sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h
  • sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_types.h
  • sdk/runanywhere-commons/src/core/events.cpp
  • sdk/runanywhere-commons/src/features/diffusion/diffusion_module.cpp
  • sdk/runanywhere-commons/src/features/embeddings/embeddings_module.cpp
  • sdk/runanywhere-commons/src/features/llm/llm_module.cpp
  • sdk/runanywhere-commons/src/features/llm/tool_calling_session.cpp
  • sdk/runanywhere-commons/src/features/lora/rac_lora_service.cpp
  • sdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cpp
  • sdk/runanywhere-commons/src/features/stt/stt_module.cpp
  • sdk/runanywhere-commons/src/features/tts/tts_module.cpp
  • sdk/runanywhere-commons/src/features/vad/vad_module.cpp
  • sdk/runanywhere-commons/src/features/vlm/vlm_module.cpp
  • sdk/runanywhere-commons/src/features/voice_agent/voice_agent_d7_abi.cpp
  • sdk/runanywhere-commons/src/features/voice_agent/voice_agent_internal_helpers.cpp
  • sdk/runanywhere-commons/src/features/voice_agent/voice_agent_internal_helpers.h
  • sdk/runanywhere-commons/src/features/voice_agent/voice_agent_proto_abi.cpp
  • sdk/runanywhere-commons/src/infrastructure/device/rac_device_manager.cpp
  • sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp
  • sdk/runanywhere-commons/src/infrastructure/network/environment.cpp
  • sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_json.cpp
  • sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp
  • sdk/runanywhere-flutter/packages/runanywhere/android/src/main/kotlin/com/runanywhere/sdk/httptransport/OkHttpHttpTransport.kt
  • sdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_auth.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_embeddings.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_stt.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_tool_calling.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_tts.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_vlm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_voice_agent.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_embeddings.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_llm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_rag.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_stt.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tools.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tts.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_voice.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Constants/SDKConstants.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+TextGeneration.ts
  • sdk/runanywhere-react-native/packages/core/src/native/NitroModulesGlobalInit.ts
  • sdk/runanywhere-web/packages/core/src/runtime/PlatformAdapter.ts
  • sdk/runanywhere-web/wasm/CMakeLists.txt
  • sdk/runanywhere-web/wasm/scripts/vendor-onnxruntime-wasm.sh
  • sdk/runanywhere-web/wasm/scripts/vendor-sherpa-onnx-wasm.sh
💤 Files with no reviewable changes (7)
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  • examples/flutter/RunAnywhereAI/android/app/src/main/res/drawable/ic_launcher_foreground.xml
  • examples/react-native/RunAnywhereAI/src/services/ModelCatalogBootstrap.ts
  • examples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsx
  • examples/react-native/RunAnywhereAI/android/gradle.properties.example
  • examples/react-native/RunAnywhereAI/android/settings.gradle

android:label="RunAnywhere AI"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

MANIFEST="examples/flutter/RunAnywhereAI/android/app/src/main/AndroidManifest.xml"

echo "Checking $MANIFEST"
rg -n 'extractNativeLibs|uses-native-library|libcdsprpc\.so' "$MANIFEST"

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 289


Fix extractNativeLibs attribute value in AndroidManifest.xml.

The file contains both required entries, but extractNativeLibs is set to "false" (line 12) when the coding guidelines require it to be "true". The <uses-native-library> entry for libcdsprpc.so is correct (line 15). Change line 12 to use android:extractNativeLibs="true".

🤖 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 `@examples/flutter/RunAnywhereAI/android/app/src/main/AndroidManifest.xml` at
line 11, Locate the android:extractNativeLibs attribute in the
AndroidManifest.xml file and change its value from "false" to "true" to comply
with the coding guidelines. This attribute controls whether native libraries
should be extracted from the APK, and it needs to be enabled for proper native
library handling.

Source: Coding guidelines

Comment on lines +122 to +130
Future<RAGStatistics> ingestDocumentAsync(RAGDocument document) async {
final session = _requireSession();
if (RacNative.bindings.rac_rag_ingest_proto == null) {
throw UnsupportedError('rac_rag_ingest_proto is unavailable');
}
final sessionAddr = session.address;
final requestBytes = document.writeToBuffer();
final resultBytes =
await Isolate.run(() => _ragIngestWorker(sessionAddr, requestBytes));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Guard the RAG session lifetime while worker isolates are running.

Both async methods capture session.address and then yield to Isolate.run; during that await, destroyPipeline() can free the native session, leaving the worker with a dangling pointer and a potential native crash. Serialize in-flight RAG operations and make destroy wait for them, or add a native ref-count/lifecycle guard before passing the pointer across isolates.

🛡️ Sketch of the required guard
 class DartBridgeRAG {
   DartBridgeRAG._();
   static final DartBridgeRAG shared = DartBridgeRAG._();

   ffi.Pointer<ffi.Void>? _session;
   bool _registered = false;
+  Future<void> _operationTail = Future<void>.value();

+  Future<T> _runWithLiveSession<T>(
+    Future<T> Function(int sessionAddr) operation,
+  ) {
+    final sessionAddr = _requireSession().address;
+    final next = _operationTail.then((_) => operation(sessionAddr));
+    _operationTail = next.then<void>((_) {}, onError: (_) {});
+    return next;
+  }
+
   Future<RAGStatistics> ingestDocumentAsync(RAGDocument document) async {
-    final session = _requireSession();
     if (RacNative.bindings.rac_rag_ingest_proto == null) {
       throw UnsupportedError('rac_rag_ingest_proto is unavailable');
     }
-    final sessionAddr = session.address;
     final requestBytes = document.writeToBuffer();
-    final resultBytes =
-        await Isolate.run(() => _ragIngestWorker(sessionAddr, requestBytes));
+    final resultBytes = await _runWithLiveSession(
+      (sessionAddr) => Isolate.run(
+        () => _ragIngestWorker(sessionAddr, requestBytes),
+      ),
+    );
     return RAGStatistics.fromBuffer(resultBytes);
   }

Apply the same guard to queryAsync, and have the destroy path await or otherwise block on _operationTail before destroying the native session.

Also applies to: 175-183

🤖 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 `@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart`
around lines 122 - 130, The ingestDocumentAsync and queryAsync methods capture
session.address and then yield to Isolate.run(), creating a window where
destroyPipeline() can be called and free the native session while the worker
still holds a dangling pointer. To fix this, implement a serialization mechanism
using _operationTail (a Future that tracks the last in-flight operation): wrap
each Isolate.run() call in both ingestDocumentAsync and queryAsync with a guard
that chains the new operation to _operationTail, update _operationTail to
complete after the isolate finishes, and modify destroyPipeline() to await
_operationTail before destroying the native session to ensure all pending RAG
operations complete first.

@sanchitmonga22

Copy link
Copy Markdown
Contributor

@coderabbitai please do a detailed review of the PR

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look at key high-complexity files mentioned in AI summaries

# 1. Check the telemetry manager changes for thread safety
rg -n "http_wakeup\|poll_http_request\|queue\|mutex" sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp | head -60

# 2. Check voice agent turn metrics helper for potential issues
cat sdk/runanywhere-commons/src/features/voice_agent/voice_agent_internal_helpers.h 2>/dev/null | head -80

# 3. Check the web download driver for potential resource leaks
rg -n "web_download_driver\|free\|delete\|cancel\|abort" sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp | head -60

# 4. Check the Flutter telemetry phase-1 attach for init ordering
rg -n "attachSinkPhase1\|initializeServices\|phase1\|Phase1\|Phase 1" sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart 2>/dev/null | head -40

# 5. Check React Native BottomSheet implementation for gesture handler dependency
cat examples/react-native/RunAnywhereAI/src/components/ui/BottomSheet.tsx 2>/dev/null | head -90

# 6. Check environment.cpp for the telemetry gate change
cat sdk/runanywhere-commons/src/infrastructure/network/environment.cpp 2>/dev/null

# 7. Check dart_bridge_device.dart for OS platform reporting changes  
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_device.dart 2>/dev/null

# 8. Check the VLM pthread-pool deadlock fix
rg -n "pthread\|deadlock\|thread_pool\|main_thread" sdk/runanywhere-commons/src/features/vlm/vlm_module.cpp 2>/dev/null | head -30

# 9. Check rag_view_model.dart for the dispose leak fix
cat examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart 2>/dev/null | head -80

# 10. Check voice_component_view_model_base.dart for the lifecycle loop fix
rg -n "resolveLoadedModel\|applyCurrentModelSnapshot\|modelLifecycle\|loaded" examples/flutter/RunAnywhereAI/lib/features/voice/voice_component_view_model_base.dart 2>/dev/null | head -50

Length of output: 47537


🏁 Script executed:

#!/bin/bash
# Look at more specific areas

# Check dart_bridge_rag.dart for potential issues with isolate pointer passing
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_rag.dart 2>/dev/null

# Check tool_calling_session.cpp for telemetry emission
cat sdk/runanywhere-commons/src/features/llm/tool_calling_session.cpp 2>/dev/null | head -80

# Check the VoiceAgentMicDriver for barge-in/drop logic
rg -n "drop\|barge\|isProcessing\|turnRequest\|stop\|cancel" sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_stt.dart 2>/dev/null | head -30

# Check ChatScreen.tsx for toolsEnabled storage key
rg -n "TOOL_CALLING_ENABLED\|APP_STORAGE_KEYS\|toolsEnabled\|AsyncStorage" examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx 2>/dev/null | head -50

# Check for potential missing null check in dart_bridge_auth.dart
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_auth.dart 2>/dev/null | head -80

# Check VLM camera view dispose order fix
cat examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart 2>/dev/null | head -60

# Check the telemetry_manager.cpp for session_id fallback logic
rg -n "sdk_session_id\|session_id\|fallback" sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp 2>/dev/null | head -40

# Check download_orchestrator.cpp for the web async driver 
rg -n "web_download_driver\|http_download\|async\|cooperative_cancel\|abort_once" sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp 2>/dev/null | head -80

# Check rac_telemetry_manager.h for poll_http_request framing docs
cat sdk/runanywhere-commons/include/rac/infrastructure/telemetry/rac_telemetry_manager.h 2>/dev/null

# Check the Flutter model lifecycle event loop fix
cat examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart 2>/dev/null | head -80

Length of output: 30814


🏁 Script executed:

#!/bin/bash
# Deep dive into specific critical paths

# 1. Full dart_bridge_telemetry.dart
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_telemetry.dart 2>/dev/null

# 2. Voice agent d7 abi - turn metrics guard
cat sdk/runanywhere-commons/src/features/voice_agent/voice_agent_d7_abi.cpp 2>/dev/null | head -120

# 3. VoiceAgentMicDriver full file
cat sdk/runanywhere-flutter/packages/runanywhere/lib/features/voice_agent/services/voice_agent_mic_driver.dart 2>/dev/null

# 4. Check model_selection.ts web SDK for hydrated event subscription
cat examples/web/RunAnywhereAI/src/components/model-selection.ts 2>/dev/null | head -80

# 5. Check download_orchestrator.cpp new driver section
rg -n "web_download_driver\|start_proto_download_worker\|async.*driver\|http_download.*avail" sdk/runanywhere-commons/src/infrastructure/download/download_orchestrator.cpp 2>/dev/null | head -50

# 6. Check react-native index.js import order
cat examples/react-native/RunAnywhereAI/index.js 2>/dev/null

# 7. Check App.tsx for staging vs production env handling
rg -n "SDK_ENVIRONMENT\|hasUsableBackendConfig\|effectiveApiKey\|staging" examples/react-native/RunAnywhereAI/App.tsx 2>/dev/null | head -50

# 8. Check the dart_bridge_llm.dart changes for sendable closure fix
cat sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart 2>/dev/null | head -80

# 9. Check the BottomSheet.tsx snapPoints conversion
rg -n "snapPoints\|containerHeight\|getHeight\|percentage" examples/react-native/RunAnywhereAI/src/components/ui/BottomSheet.tsx 2>/dev/null

# 10. Check for any missing exports/imports in theme system index.ts
cat examples/react-native/RunAnywhereAI/src/theme/system/index.ts 2>/dev/null

Length of output: 39891


🏁 Script executed:

#!/bin/bash
# Check PR diff stats and get additional context

# 1. Check if there are test files for new functionality
fd -e test.dart -e spec.ts -e test.cpp . | grep -E "(telemetry|voice_agent|download|rag|embeddings)" | head -20

# 2. Check the proto ABI for session_id propagation in voice agent
rg -n "session_id\|sessionId" sdk/runanywhere-commons/src/features/voice_agent/ 2>/dev/null | head -30

# 3. Check for null pointer safety in voice_agent_internal_helpers
rg -n "nullptr\|null.*check\|if.*session_id\|if.*model_id" sdk/runanywhere-commons/src/features/voice_agent/voice_agent_internal_helpers.cpp 2>/dev/null | head -30

# 4. Check the vad_module.cpp key logic for accumulator flush
rg -n "flush\|open.*utterance\|utterance.*state\|VadUtteranceState" sdk/runanywhere-commons/src/features/vad/vad_module.cpp 2>/dev/null | head -40

# 5. Check the ConversationListScreen for visible prop passed correctly
rg -n "ConversationListScreen\|visible.*conversation\|conversation.*visible" examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx 2>/dev/null | head -20

# 6. Check for memory leaks in embeddings isolate worker
rg -n "free\|finally\|RacProtoBuffer\|bindings\|rac_proto_buffer" sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_embeddings.dart 2>/dev/null | head -40

# 7. Check DefaultConfig usage in runanywhere_ai_app.dart
rg -n "DefaultConfig\|runanywhereApiKey\|runanywhereBaseUrl\|hasCustomConfig" examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart 2>/dev/null | head -30

# 8. Check if rac_auth_get_access_token is declared/exported in web SDK
rg -rn "rac_auth_get_access_token" . | head -20

# 9. Check the telemetry_manager for queue thread safety
rg -n "queue\|mutex\|lock\|http_queue\|enqueue" sdk/runanywhere-commons/src/infrastructure/telemetry/telemetry_manager.cpp 2>/dev/null | head -40

# 10. Check diff stat of the PR (lines changed)
gh pr view 511 --repo RunanywhereAI/runanywhere-sdks --json additions,deletions,changedFiles 2>/dev/null || echo "PR stats not available"

Length of output: 3593


🏁 Script executed:

Length of output: 91



Here are the results from shell you requested:

Shell block treated as final reply content because the matching action tag was not present.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (4)
examples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dart (1)

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

Cancel/guard streaming work when the ViewModel is disposed.

describeSelectedImage() can keep receiving tokens after the screen is popped, but the owning widget disposes this ChangeNotifier. Add a disposed guard and cancel any in-flight VLM generation on dispose() to avoid updates after teardown and leaked native work.

🛡️ Proposed fix
   bool _isProcessing = false;
   String _currentDescription = '';
   String? _error;
   String? _selectedImagePath;
   String _prompt = 'Describe this image in detail.';
+  bool _disposed = false;
@@
-    notifyListeners();
+    _notify();
@@
-      notifyListeners();
+      _notify();
@@
-      notifyListeners();
+      _notify();
@@
-    notifyListeners();
+    _notify();
@@
-    notifyListeners();
+    _notify();
@@
         buffer.write(event.token);
         _currentDescription = buffer.toString();
-        notifyListeners();
+        _notify();
       }
@@
       debugPrint('VLM describe error: $e');
       _error = e.toString();
-      notifyListeners();
+      _notify();
     } finally {
       _isProcessing = false;
-      notifyListeners();
+      _notify();
     }
   }
+
+  void _notify() {
+    if (!_disposed) {
+      notifyListeners();
+    }
+  }
+
+  `@override`
+  void dispose() {
+    _disposed = true;
+    if (_isProcessing) {
+      unawaited(
+        sdk.RunAnywhere.vlm.cancelVLMGeneration().catchError((Object e) {
+          debugPrint('VLM cancel during dispose failed: $e');
+        }),
+      );
+    }
+    super.dispose();
+  }

Also applies to: 131-134

🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dart`
around lines 88 - 126, The describeSelectedImage() method can continue receiving
and processing tokens even after the ViewModel is disposed, causing UI updates
and potential memory leaks. Override the dispose() method in this ChangeNotifier
class to cancel any in-flight VLM generation work. Within the
describeSelectedImage() method's await for loop that processes tokens from the
stream, add a guard check to break early if the ViewModel has been disposed.
Store a reference to the stream subscription so it can be cancelled in
dispose(). Apply the same disposal guard pattern to the related method on lines
131-134 to ensure consistent cleanup across all streaming operations.
examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx (3)

157-185: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard against double-starting the mic session.

isSessionActive is only set after driver.start() resolves, so rapid taps can start multiple VoiceAgentMicDriver instances and overwrite micDriverRef, leaving an earlier recorder outside cleanup ownership.

Suggested fix
   const micDriverRef = useRef<VoiceAgentMicDriver | null>(null);
   const scrollRef = useRef<ScrollView>(null);
+  const startInFlightRef = useRef(false);
@@
   const handleToggleSession = useCallback(async () => {
-    if (status === VoicePipelineStatus.Processing || status === VoicePipelineStatus.Thinking) {
+    if (
+      startInFlightRef.current ||
+      status === VoicePipelineStatus.Processing ||
+      status === VoicePipelineStatus.Thinking
+    ) {
       return;
     }
@@
     }
     try {
+      startInFlightRef.current = true;
       // Compose the pipeline against the loaded models. ensureVAD defaults to
       // true → loads silero-vad so the C++ agent uses the model VAD.
       await RunAnywhere.initializeVoiceAgentWithLoadedModels();
@@
     } catch (error) {
       console.error('[VoiceAssistant] Failed to start voice agent:', error);
       await cleanupVoiceSession();
       Alert.alert('Error', `Failed to start voice agent: ${error}`);
+    } finally {
+      startInFlightRef.current = false;
     }
   }, [isSessionActive, allModelsLoaded, status, appendTurn, phaseToStatus, cleanupVoiceSession]);

Also applies to: 203-210

🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`
around lines 157 - 185, The handleToggleSession callback is vulnerable to race
conditions where rapid taps can start multiple VoiceAgentMicDriver instances
before isSessionActive is set to true. Since isSessionActive is only updated
after driver.start() resolves, the guard clause checking isSessionActive at the
beginning of handleToggleSession cannot prevent re-entry. Fix this by setting
isSessionActive to true immediately before awaiting driver.start(), so that
subsequent rapid taps will be blocked by the existing guard condition. This
ensures that only one VoiceAgentMicDriver instance is active and prevents
orphaned recorders from being left outside cleanup ownership.

167-171: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include VAD in the required-model alert.

Line 57 now requires vadModel, but the alert still tells users only STT, LLM, and TTS are required.

Suggested fix
       Alert.alert(
         'Models Required',
-        'Please load all required models (STT, LLM, TTS) to use the voice assistant.'
+        'Please load all required models (STT, LLM, TTS, VAD) to use the voice assistant.'
       );
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`
around lines 167 - 171, The alert message displayed when allModelsLoaded is
false is outdated and does not mention the VAD model requirement that was added
on line 57. Update the Alert.alert call with title 'Models Required' to include
VAD in the list of required models. Modify the message string to reference all
four required models: STT, LLM, TTS, and VAD instead of just the original three.

424-439: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add accessibility metadata to icon-only buttons.

The mic and clear controls render only icons, so screen-reader users do not get actionable labels. Add roles, labels, and disabled state.

Suggested fix
         <TouchableOpacity
           style={[styles.micButton, { backgroundColor: micButtonColor() }]}
           onPress={handleToggleSession}
           disabled={!micButtonEnabled}
           activeOpacity={0.8}
+          accessibilityRole="button"
+          accessibilityLabel={isSessionActive ? 'Stop voice assistant' : 'Start voice assistant'}
+          accessibilityState={{ disabled: !micButtonEnabled }}
         >
@@
         {conversation.length > 0 && (
-          <TouchableOpacity onPress={handleClear} hitSlop={8} style={styles.clearBtn}>
+          <TouchableOpacity
+            onPress={handleClear}
+            hitSlop={8}
+            style={styles.clearBtn}
+            accessibilityRole="button"
+            accessibilityLabel="Clear voice conversation"
+          >
             <Icon name="trash" size={dimens.icon.sm} color={colors.onSurfaceVariant} />
           </TouchableOpacity>
         )}
🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`
around lines 424 - 439, The mic button (TouchableOpacity with
onPress={handleToggleSession}) and clear button (TouchableOpacity with
onPress={handleClear}) are icon-only controls that lack accessibility metadata,
making them unusable for screen reader users. Add the accessible prop set to
true, an accessibilityLabel prop describing the button's purpose (such as
"Toggle voice session" for the mic button and "Clear conversation" for the clear
button), and an accessibilityState prop with the disabled state to the mic
button to reflect its disabled prop value.
🧹 Nitpick comments (2)
examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart (1)

38-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use ListenableBuilder instead of manual ViewModel listener plumbing.

This screen still registers a listener and calls setState for ViewModel updates. Wrap the Scaffold in ListenableBuilder and remove _onViewModelChanged/listener cleanup.

♻️ Proposed refactor
     _viewModel = VLMViewModel();
     _promptController = TextEditingController(text: _viewModel.prompt);
-    _viewModel.addListener(_onViewModelChanged);
     unawaited(_viewModel.checkModelStatus());
   }
 
-  void _onViewModelChanged() {
-    if (mounted) {
-      setState(() {});
-    }
-  }
-
   `@override`
   void dispose() {
-    _viewModel.removeListener(_onViewModelChanged);
     _viewModel.dispose();
     _promptController.dispose();
     super.dispose();
   }
 
   `@override`
   Widget build(BuildContext context) {
-    return Scaffold(
-      appBar: _buildAppBar(),
-      body: _viewModel.isModelLoaded
-          ? _buildMainContent()
-          : _buildModelRequiredContent(),
+    return ListenableBuilder(
+      listenable: _viewModel,
+      builder: (context, _) {
+        return Scaffold(
+          appBar: _buildAppBar(),
+          body: _viewModel.isModelLoaded
+              ? _buildMainContent()
+              : _buildModelRequiredContent(),
+        );
+      },
     );
   }

As per coding guidelines, examples/flutter/RunAnywhereAI/lib/**/*.dart: “Use ChangeNotifier + ListenableBuilder pattern for feature-level state management.”

🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`
around lines 38 - 62, Remove the manual listener plumbing from the VlmCameraView
class. Delete the _viewModel.addListener(_onViewModelChanged) call from
initState, remove the _onViewModelChanged method entirely, and remove the
_viewModel.removeListener(_onViewModelChanged) call from dispose. In the build
method, wrap the Scaffold widget and all its content with ListenableBuilder,
passing _viewModel as the listenable parameter and moving the current Scaffold
structure into the builder callback function. This aligns with the
ChangeNotifier + ListenableBuilder pattern guidelines.

Source: Coding guidelines

examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx (1)

347-441: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Move inline style objects out of the render tree.

The new JSX adds many inline object literals for theme values. Please hoist these into a themed style factory or memoized style map so the screen follows the React Native example styling rule and avoids repeated per-render style allocations.

As per coding guidelines, examples/react-native/RunAnywhereAI/**/*.{ts,tsx} should “Avoid inline styles; use centralized theme system (colors.ts, typography.ts, spacing.ts) instead.”

🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`
around lines 347 - 441, The render method contains numerous inline style objects
(like backgroundColor, paddingTop, borderRadius, textAlign, etc.) that are
recreated on every render, causing unnecessary performance overhead. Extract all
inline style definitions from the JSX into a memoized style factory using
useMemo that depends on the theme dependencies (colors, typography, dimens,
insets). Create a single styles object within useMemo that returns an organized
map of all styles needed for the setupCard, divider, conversationArea,
emptyState, scrollView, error message area, controls section, micButton, and
clearBtn elements. Then replace each inline style object in the View and Text
components with references to the corresponding style from this centralized
memoized styles map.

Source: Coding guidelines

🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`:
- Around line 184-189: The _pickImage method performs an asynchronous operation
with the image picker, and if the user navigates away while the picker is open,
the widget can be disposed before the picker returns. Add a check for the
mounted property before calling _viewModel.setSelectedImage(xFile.path) to
ensure the widget is still active and part of the widget tree before attempting
to update the ViewModel. This prevents attempting to update a disposed object.
- Around line 72-82: The Text widget displaying _viewModel.loadedModelName! can
overflow the AppBar when model names are long. Wrap the Text widget in a
SizedBox with a maxWidth constraint to limit the width, and add overflow:
TextOverflow.ellipsis to the Text widget's styling to handle text overflow
gracefully with an ellipsis. This will prevent the model name from overflowing
the AppBar actions row while maintaining readability.

In `@examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart`:
- Around line 267-277: After setup is complete (when allModelsLoaded is true),
the active Voice Models menu displays STT, LLM, and TTS model controls but
currently omits VAD. Locate the section in the code where the Voice Models
menu/model-info is rendered after allModelsLoaded becomes true, and add a
_buildModelConfigRow call for VAD using _viewModel.currentVADModel,
_viewModel.vadModelState, the VAD color (AppColors.statusOrange), and the
_showVADModelSelection handler, mirroring the VAD configuration row shown during
the initial setup phase.
- Around line 584-628: The bubble UI in the voice_assistant_view.dart file lacks
semantic information for screen readers to announce the speaker role. Wrap the
Text widget displaying turn.text with a Semantics widget that includes a label
parameter to announce the speaker identity, such as "User message" or "Assistant
message" based on the isUser variable, ensuring that assistive technology users
can understand who is speaking even without visual color and alignment cues.

In `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`:
- Around line 59-62: Store the timeout ID from the delayed error reset (around
lines 188-191) in a ref, and then clear that timeout in the cleanupVoiceSession
function before setting micDriverRef.current to null. This prevents the timeout
from firing after cleanup and resetting the screen state to "Listening" when the
session has already stopped or the component has unmounted.

---

Outside diff comments:
In `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dart`:
- Around line 88-126: The describeSelectedImage() method can continue receiving
and processing tokens even after the ViewModel is disposed, causing UI updates
and potential memory leaks. Override the dispose() method in this ChangeNotifier
class to cancel any in-flight VLM generation work. Within the
describeSelectedImage() method's await for loop that processes tokens from the
stream, add a guard check to break early if the ViewModel has been disposed.
Store a reference to the stream subscription so it can be cancelled in
dispose(). Apply the same disposal guard pattern to the related method on lines
131-134 to ensure consistent cleanup across all streaming operations.

In `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`:
- Around line 157-185: The handleToggleSession callback is vulnerable to race
conditions where rapid taps can start multiple VoiceAgentMicDriver instances
before isSessionActive is set to true. Since isSessionActive is only updated
after driver.start() resolves, the guard clause checking isSessionActive at the
beginning of handleToggleSession cannot prevent re-entry. Fix this by setting
isSessionActive to true immediately before awaiting driver.start(), so that
subsequent rapid taps will be blocked by the existing guard condition. This
ensures that only one VoiceAgentMicDriver instance is active and prevents
orphaned recorders from being left outside cleanup ownership.
- Around line 167-171: The alert message displayed when allModelsLoaded is false
is outdated and does not mention the VAD model requirement that was added on
line 57. Update the Alert.alert call with title 'Models Required' to include VAD
in the list of required models. Modify the message string to reference all four
required models: STT, LLM, TTS, and VAD instead of just the original three.
- Around line 424-439: The mic button (TouchableOpacity with
onPress={handleToggleSession}) and clear button (TouchableOpacity with
onPress={handleClear}) are icon-only controls that lack accessibility metadata,
making them unusable for screen reader users. Add the accessible prop set to
true, an accessibilityLabel prop describing the button's purpose (such as
"Toggle voice session" for the mic button and "Clear conversation" for the clear
button), and an accessibilityState prop with the disabled state to the mic
button to reflect its disabled prop value.

---

Nitpick comments:
In `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`:
- Around line 38-62: Remove the manual listener plumbing from the VlmCameraView
class. Delete the _viewModel.addListener(_onViewModelChanged) call from
initState, remove the _onViewModelChanged method entirely, and remove the
_viewModel.removeListener(_onViewModelChanged) call from dispose. In the build
method, wrap the Scaffold widget and all its content with ListenableBuilder,
passing _viewModel as the listenable parameter and moving the current Scaffold
structure into the builder callback function. This aligns with the
ChangeNotifier + ListenableBuilder pattern guidelines.

In `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`:
- Around line 347-441: The render method contains numerous inline style objects
(like backgroundColor, paddingTop, borderRadius, textAlign, etc.) that are
recreated on every render, causing unnecessary performance overhead. Extract all
inline style definitions from the JSX into a memoized style factory using
useMemo that depends on the theme dependencies (colors, typography, dimens,
insets). Create a single styles object within useMemo that returns an organized
map of all styles needed for the setupCard, divider, conversationArea,
emptyState, scrollView, error message area, controls section, micButton, and
clearBtn elements. Then replace each inline style object in the View and Text
components with references to the corresponding style from this centralized
memoized styles map.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62592583-ee59-4ea2-b4c8-fb1a5ab1e8eb

📥 Commits

Reviewing files that changed from the base of the PR and between 2bbba92 and eb0bd78.

📒 Files selected for processing (12)
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/voice/VoiceScreen.kt
  • examples/android/RunAnywhereAI/gradle/libs.versions.toml
  • examples/android/RunAnywhereAI/gradle/wrapper/gradle-wrapper.properties
  • examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/vision/vlm_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_agent_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart
  • examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx
  • sdk/runanywhere-flutter/packages/runanywhere/lib/features/tts/services/audio_playback_manager.dart
  • sdk/runanywhere-react-native/packages/core/src/Features/VoiceSession/AudioPlaybackManager.ts
  • sdk/runanywhere-react-native/packages/core/src/Features/VoiceSession/VoiceAgentMicDriver.ts
  • sdk/runanywhere-react-native/packages/core/src/index.ts
✅ Files skipped from review due to trivial changes (1)
  • examples/android/RunAnywhereAI/gradle/wrapper/gradle-wrapper.properties

Comment on lines 72 to 82
if (_viewModel.loadedModelName != null)
Padding(
padding: const EdgeInsets.only(right: AppSpacing.large),
child: Center(
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.small),
child: Text(
_viewModel.loadedModelName!,
style: AppTypography.caption(context).copyWith(
color: Colors.grey,
),
style: AppTypography.caption(context)
.copyWith(color: AppColors.textSecondary(context)),
),
),
),

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 | 🟡 Minor | ⚡ Quick win

Constrain the AppBar model label.

Long model names/IDs can overflow the AppBar actions row. Add a width cap plus ellipsis.

💡 Proposed fix
           Center(
             child: Padding(
               padding: const EdgeInsets.symmetric(horizontal: AppSpacing.small),
-              child: Text(
-                _viewModel.loadedModelName!,
-                style: AppTypography.caption(context)
-                    .copyWith(color: AppColors.textSecondary(context)),
+              child: ConstrainedBox(
+                constraints: const BoxConstraints(maxWidth: 160),
+                child: Text(
+                  _viewModel.loadedModelName!,
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                  style: AppTypography.caption(context)
+                      .copyWith(color: AppColors.textSecondary(context)),
+                ),
               ),
             ),
           ),
🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`
around lines 72 - 82, The Text widget displaying _viewModel.loadedModelName! can
overflow the AppBar when model names are long. Wrap the Text widget in a
SizedBox with a maxWidth constraint to limit the width, and add overflow:
TextOverflow.ellipsis to the Text widget's styling to handle text overflow
gracefully with an ellipsis. This will prevent the model name from overflowing
the AppBar actions row while maintaining readability.

Comment on lines +184 to +189
Future<void> _pickImage(ImageSource source) async {
try {
final xFile = await _picker.pickImage(source: source);
if (xFile != null) {
_viewModel.setSelectedImage(xFile.path);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Check mounted after the OS picker returns.

If the user leaves the screen while the picker is open, this success path can update a disposed ViewModel.

🛡️ Proposed fix
       final xFile = await _picker.pickImage(source: source);
+      if (!mounted) return;
       if (xFile != null) {
         _viewModel.setSelectedImage(xFile.path);
       }
🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/vision/vlm_camera_view.dart`
around lines 184 - 189, The _pickImage method performs an asynchronous operation
with the image picker, and if the user navigates away while the picker is open,
the widget can be disposed before the picker returns. Add a check for the
mounted property before calling _viewModel.setSelectedImage(xFile.path) to
ensure the widget is still active and part of the widget tree before attempting
to update the ViewModel. This prevents attempting to update a disposed object.

Comment on lines +267 to +277
const SizedBox(height: AppSpacing.large),

// VAD Model
_buildModelConfigRow(
icon: Icons.hearing,
label: 'Voice Activity (VAD)',
modelName: _viewModel.currentVADModel,
state: _viewModel.vadModelState,
color: AppColors.statusOrange,
onTap: _showVADModelSelection,
),

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 | 🟡 Minor | ⚡ Quick win

Expose VAD controls after setup too.

Once allModelsLoaded is true, this setup view disappears, but the active “Voice Models” menu/model-info section still only shows STT, LLM, and TTS. Add VAD there as well so users can inspect or change the required VAD model without leaving the flow.

💡 Proposed UI wiring
           ListTile(
             leading: const Icon(
               Icons.volume_up,
               color: AppColors.primaryPurple,
             ),
             title: const Text('Text-to-Speech'),
             subtitle: Text(_viewModel.currentTTSModel),
             trailing: const Icon(Icons.chevron_right),
             onTap: () {
               Navigator.pop(context);
               _showTTSModelSelection();
             },
           ),
+          ListTile(
+            leading: const Icon(Icons.hearing, color: AppColors.statusOrange),
+            title: const Text('Voice Activity (VAD)'),
+            subtitle: Text(_viewModel.currentVADModel),
+            trailing: const Icon(Icons.chevron_right),
+            onTap: () {
+              Navigator.pop(context);
+              _showVADModelSelection();
+            },
+          ),
           const SizedBox(height: AppSpacing.large),
           _ModelBadge(
             icon: Icons.volume_up,
             label: 'TTS',
             value: _viewModel.currentTTSModel,
             color: AppColors.primaryPurple,
           ),
+          _ModelBadge(
+            icon: Icons.hearing,
+            label: 'VAD',
+            value: _viewModel.currentVADModel,
+            color: AppColors.statusOrange,
+          ),
🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart`
around lines 267 - 277, After setup is complete (when allModelsLoaded is true),
the active Voice Models menu displays STT, LLM, and TTS model controls but
currently omits VAD. Locate the section in the code where the Voice Models
menu/model-info is rendered after allModelsLoaded becomes true, and add a
_buildModelConfigRow call for VAD using _viewModel.currentVADModel,
_viewModel.vadModelState, the VAD color (AppColors.statusOrange), and the
_showVADModelSelection handler, mirroring the VAD configuration row shown during
the initial setup phase.

Comment on lines +584 to +628
const radius = AppSpacing.cornerRadiusBubble;

return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.large),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
speaker,
style: AppTypography.caption(context).copyWith(
color: AppColors.textSecondary(context),
fontWeight: FontWeight.w500,
),
final bubble = ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.76,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.large,
vertical: AppSpacing.mediumLarge,
),
decoration: BoxDecoration(
gradient: isUser
? LinearGradient(
colors: [
AppColors.userBubbleGradientStart,
AppColors.userBubbleGradientEnd,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: isUser ? null : AppColors.assistantBubbleBg(context),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(radius),
topRight: const Radius.circular(radius),
bottomLeft: Radius.circular(isUser ? radius : 4),
bottomRight: Radius.circular(isUser ? 4 : radius),
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(AppSpacing.mediumLarge),
decoration: BoxDecoration(
color: isUser
? AppColors.backgroundGray5(context)
: AppColors.primaryBlue.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(
AppSpacing.cornerRadiusBubble,
),
),
child: Text(turn.text, style: AppTypography.body(context)),
),
child: Text(
turn.text,
style: AppTypography.body(context).copyWith(
color: isUser ? Colors.white : AppColors.textPrimary(context),
),
],
),
),
);

return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge),
child: Row(
mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [bubble],

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 | 🟡 Minor | ⚡ Quick win

Preserve speaker roles for assistive tech.

The new bubble UI removes the speaker label, leaving user/assistant identity conveyed only by alignment and color. Add semantics so screen readers still announce the role.

♿ Proposed fix
-    return Padding(
-      padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge),
-      child: Row(
-        mainAxisAlignment:
-            isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
-        children: [bubble],
+    return Semantics(
+      label: '${isUser ? 'You' : 'Assistant'}: ${turn.text}',
+      child: ExcludeSemantics(
+        child: Padding(
+          padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge),
+          child: Row(
+            mainAxisAlignment:
+                isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
+            children: [bubble],
+          ),
+        ),
       ),
     );
📝 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 radius = AppSpacing.cornerRadiusBubble;
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.large),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
speaker,
style: AppTypography.caption(context).copyWith(
color: AppColors.textSecondary(context),
fontWeight: FontWeight.w500,
),
final bubble = ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.76,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.large,
vertical: AppSpacing.mediumLarge,
),
decoration: BoxDecoration(
gradient: isUser
? LinearGradient(
colors: [
AppColors.userBubbleGradientStart,
AppColors.userBubbleGradientEnd,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: isUser ? null : AppColors.assistantBubbleBg(context),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(radius),
topRight: const Radius.circular(radius),
bottomLeft: Radius.circular(isUser ? radius : 4),
bottomRight: Radius.circular(isUser ? 4 : radius),
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(AppSpacing.mediumLarge),
decoration: BoxDecoration(
color: isUser
? AppColors.backgroundGray5(context)
: AppColors.primaryBlue.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(
AppSpacing.cornerRadiusBubble,
),
),
child: Text(turn.text, style: AppTypography.body(context)),
),
child: Text(
turn.text,
style: AppTypography.body(context).copyWith(
color: isUser ? Colors.white : AppColors.textPrimary(context),
),
],
),
),
);
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge),
child: Row(
mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [bubble],
const radius = AppSpacing.cornerRadiusBubble;
final bubble = ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.76,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.large,
vertical: AppSpacing.mediumLarge,
),
decoration: BoxDecoration(
gradient: isUser
? LinearGradient(
colors: [
AppColors.userBubbleGradientStart,
AppColors.userBubbleGradientEnd,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: isUser ? null : AppColors.assistantBubbleBg(context),
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(radius),
topRight: const Radius.circular(radius),
bottomLeft: Radius.circular(isUser ? radius : 4),
bottomRight: Radius.circular(isUser ? 4 : radius),
),
),
child: Text(
turn.text,
style: AppTypography.body(context).copyWith(
color: isUser ? Colors.white : AppColors.textPrimary(context),
),
),
),
);
return Semantics(
label: '${isUser ? 'You' : 'Assistant'}: ${turn.text}',
child: ExcludeSemantics(
child: Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.mediumLarge),
child: Row(
mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [bubble],
),
),
),
);
🤖 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 `@examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart`
around lines 584 - 628, The bubble UI in the voice_assistant_view.dart file
lacks semantic information for screen readers to announce the speaker role. Wrap
the Text widget displaying turn.text with a Semantics widget that includes a
label parameter to announce the speaker identity, such as "User message" or
"Assistant message" based on the isUser variable, ensuring that assistive
technology users can understand who is speaking even without visual color and
alignment cues.

Comment on lines 59 to +62
const cleanupVoiceSession = useCallback(async () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
if (micDriverRef.current) {
micDriverRef.current.stop();
micDriverRef.current = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clear the delayed error reset during cleanup.

Line 191 can set the screen back to Listening after the user stops the session or the component unmounts. Store the timeout and clear it in cleanupVoiceSession; only reset if a driver is still active.

Suggested fix
   const micDriverRef = useRef<VoiceAgentMicDriver | null>(null);
   const scrollRef = useRef<ScrollView>(null);
+  const errorResetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
 
   const allModelsLoaded = sttModel && llmModel && ttsModel && vadModel;
 
   const cleanupVoiceSession = useCallback(async () => {
+    if (errorResetTimerRef.current) {
+      clearTimeout(errorResetTimerRef.current);
+      errorResetTimerRef.current = null;
+    }
     if (micDriverRef.current) {
       micDriverRef.current.stop();
       micDriverRef.current = null;
@@
         onError: (err) => {
           console.error('[VoiceAssistant] Voice turn error:', err);
           setStatus(VoicePipelineStatus.Error);
-          setTimeout(() => setStatus(VoicePipelineStatus.Listening), 2000);
+          if (errorResetTimerRef.current) {
+            clearTimeout(errorResetTimerRef.current);
+          }
+          errorResetTimerRef.current = setTimeout(() => {
+            if (micDriverRef.current) {
+              setStatus(VoicePipelineStatus.Listening);
+            }
+            errorResetTimerRef.current = null;
+          }, 2000);
         },

Also applies to: 188-191

🤖 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 `@examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx`
around lines 59 - 62, Store the timeout ID from the delayed error reset (around
lines 188-191) in a ref, and then clear that timeout in the cleanupVoiceSession
function before setting micDriverRef.current to null. This prevents the timeout
from firing after cleanup and resetting the screen state to "Listening" when the
session has already stopped or the component has unmounted.

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