Skip to content

Cleanup V2 transition + testing + release#504

Open
sanchitmonga22 wants to merge 9 commits into
mainfrom
feat/v2-architecture
Open

Cleanup V2 transition + testing + release#504
sanchitmonga22 wants to merge 9 commits into
mainfrom
feat/v2-architecture

Conversation

@sanchitmonga22

@sanchitmonga22 sanchitmonga22 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Description

Brief description of the changes made.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Refactoring

Testing

  • Lint passes locally
  • Added/updated tests for changes

Platform-Specific Testing (check all that apply)

Swift SDK / iOS Sample:

  • Tested on iPhone (Simulator or Device)
  • Tested on iPad / Tablet
  • Tested on Mac (macOS target)

Kotlin SDK / Android Sample:

  • Tested on Android Phone (Emulator or Device)
  • Tested on Android Tablet

Flutter SDK / Flutter Sample:

  • Tested on iOS
  • Tested on Android

React Native SDK / React Native Sample:

  • Tested on iOS
  • Tested on Android

Playground:

  • Tested on target platform
  • Verified no regressions in existing Playground projects
    Web SDK / Web Sample:
  • Tested in Chrome (Desktop)
  • Tested in Firefox
  • Tested in Safari
  • WASM backends load (LlamaCpp + ONNX)
  • OPFS storage persistence verified (survives page refresh)
  • Settings persistence verified (localStorage)

Labels

Please add the appropriate label(s):

SDKs:

  • Swift SDK - Changes to Swift SDK (sdk/runanywhere-swift)
  • Kotlin SDK - Changes to Kotlin SDK (sdk/runanywhere-kotlin)
  • Flutter SDK - Changes to Flutter SDK (sdk/runanywhere-flutter)
  • React Native SDK - Changes to React Native SDK (sdk/runanywhere-react-native)
  • Web SDK - Changes to Web SDK (sdk/runanywhere-web)
  • Commons - Changes to shared native code (sdk/runanywhere-commons)

Sample Apps:

  • iOS Sample - Changes to iOS example app (examples/ios)
  • Android Sample - Changes to Android example app (examples/android)
  • Flutter Sample - Changes to Flutter example app (examples/flutter)
  • React Native Sample - Changes to React Native example app (examples/react-native)
  • Web Sample - Changes to Web example app (examples/web)

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Documentation updated (if needed)

Screenshots

Attach relevant UI screenshots for changes (if applicable):

  • Mobile (Phone)
  • Tablet / iPad
  • Desktop / Mac

Note

Medium Risk
SDK init ordering, JNI HTTP/error parsing, and hybrid STT audio normalization affect core Android integration paths; changes are mostly example-app and parity fixes with bounded blast radius.

Overview
Aligns the Android RunAnywhereAI sample with the iOS example and hardens Kotlin SDK parity with Swift/commons.

Android sample: LlamaCPP/ONNX register before RunAnywhere.initialize(); failed setup surfaces InitErrorScreen with retry. Model bootstrap re-registers the full catalog each launch (merge preserves downloads), calls refreshModelRegistry(), and fixes catalog byte sizes/URLs. Chat gains conversation analytics, SDK event–driven TTFT/metrics, smart titles, full-text history search, persisted tool-calling, and restored conversation model labels. New VAD, Tool Calling, and Solutions (YAML demo) flows; benchmarks unload models between scenarios and track memory like iOS. Hybrid STT sends raw PCM (SDK wraps WAV).

Kotlin SDK / JNI: Adds racApiErrorFromResponse and racModelIdFromUrl; HTTP client maps structured API errors and OkHttp timeouts match URLSession. Hybrid STT normalizes raw PCM16 to WAV for shared offline/online dispatch; pcm16ToWav is public. Init/reset wires CppBridgeState (including racStateReset on shutdown). Logging gates trace/debug in release; structured output and stream cancel/aggregation behavior tightened.

iOS sample (smaller): Seeds the abliterated LoRA adapter; Qwen 0.5B marked LoRA-compatible; chat analytics TTFT from events; voice agent calls cleanupVoiceAgent() on stop/teardown.

Reviewed by Cursor Bugbot for commit 009017d. Configure here.

Summary by CodeRabbit

  • New Features

    • Voice Activity Detection (VAD) for real-time speech monitoring
    • Hybrid Speech-to-Text combining offline and cloud transcription
    • Tool Calling enabling AI models to execute registered functions
    • LoRA adapter support for model fine-tuning
    • Chat analytics displaying message generation metrics
    • Benchmarking suite for performance testing across modalities
    • Solutions YAML runner for predefined workflows
    • Smart conversation title generation
    • Voice Agent improvements with better session management
  • Improvements

    • Enhanced error recovery and initialization handling
    • Better resource cleanup and memory management
    • Improved model registry and storage management
    • Refined UI/UX across chat and settings screens

runanywhere and others added 2 commits June 10, 2026 01:50
Kotlin: model IDs now derived via commons rac_model_id_from_url (new JNI
thunk) instead of two divergent slug heuristics; registration surface
deduped to Storage-only like Swift; rac_sdk_init wired into Phase 1;
reset() now clears C++ state + auth; tool-calling merges LLM options
(top_p no longer dropped); aggregateStream stops at terminal event and
reports full metrics; structured-output, download persist, RAG, LoRA,
VAD/VoiceAgent error shapes, HTTP 403/forbidden + structured API errors
(new rac_api_error_from_response thunk), timeouts, and logging presets
all mirror Swift. Swift: ported Kotlin's typed EventBus payload
publishers and sherpa-plugin pre-flight in HybridSTTRouter.

Co-authored-by: Cursor <cursoragent@cursor.com>
… into SDK hybrid STT

Android app: register backends before initialize() (fixes the -422 race
iOS documents), drop app-side SystemTTS registration (SDK Phase 2 owns
it), sync catalog data to iOS values (silero-vad 2.3MB guard fix,
smollm2 size, minilm vocab URL), re-register catalog each launch +
refreshModelRegistry, retryable init-failure screen, benchmark unloads
per scenario with iOS scenario set, and new iOS-parity surfaces: VAD
demo, Tool Calling settings (persisted toggle), Solutions YAML demo,
conversation search/smart titles/model-per-conversation, SDK-event
analytics + chat details sheet.

iOS app: cleanupVoiceAgent on stop/teardown, persist SDK-event TTFT +
real generationMode, thread disableThinking through chat options,
seed the abliterated LoRA adapter; supportsLora moved to qwen-0.5b on
both platforms (matches the adapter's actual base model).

SDKs: hybrid STT routers (Kotlin+Swift) now normalize raw PCM16 to WAV
internally via new mirrored pcm16ToWav helpers; the app passes raw PCM.

Co-authored-by: Cursor <cursoragent@cursor.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@sanchitmonga22 sanchitmonga22 changed the title Feat/v2 architecture Cleanup V2 transition + testing + release Jun 10, 2026

@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 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for 2 of the 3 issues found in the latest run.

  • ✅ Fixed: Analytics success rate miscalculation
    • The success rate now compares assistant replies with visible content against total assistant messages instead of unrelated stats rows.
  • ✅ Fixed: Retry allows parallel setup
    • A setup mutex now rejects concurrent SDK setup attempts so retries cannot overlap initialization work.

Create PR

Or push these changes by commenting:

@cursor push a175c3554c
Preview (a175c3554c)
diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt
--- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt
+++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt
@@ -20,11 +20,13 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
 import kotlin.coroutines.cancellation.CancellationException
 
 class RunAnywhereApplication : Application() {
 
     private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+    private val setupMutex = Mutex()
 
     override fun onCreate() {
         super.onCreate()
@@ -48,6 +50,11 @@
     }
 
     private suspend fun runSdkSetup() {
+        if (!setupMutex.tryLock()) {
+            RACLog.i("SDK setup already in progress")
+            return
+        }
+
         try {
             setupSDK()
             GlobalState.markReady()
@@ -56,6 +63,8 @@
         } catch (e: Throwable) {
             RACLog.e("SDK setup failed", e)
             GlobalState.markInitFailed(e.message ?: e.javaClass.simpleName)
+        } finally {
+            setupMutex.unlock()
         }
     }
 

diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.kt
--- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.kt
+++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.kt
@@ -71,8 +71,8 @@
                 DetailRow("Avg Response", String.format(Locale.US, "%.1fs", avgTimeSec))
                 DetailRow("Token Speed", "${avgSpeed.toInt()} tok/s")
                 DetailRow("Total Tokens", "$totalTokens")
-                if (repliesWithContent > 0) {
-                    DetailRow("Success Rate", "${stats.size * 100 / repliesWithContent}%")
+                if (assistantMessages.isNotEmpty()) {
+                    DetailRow("Success Rate", "${repliesWithContent * 100 / assistantMessages.size}%")
                 }
 
                 SectionHeader("Models")

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 009017d. Configure here.

} catch (e: Exception) {
RACLog.e("core backends failed", e)
}
RunAnywhere.refreshModelRegistry()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

System TTS registration race

Medium Severity

Removing synchronous SystemTTSModule.register() from bootstrap means the built-in system-tts catalog entry is only registered during SDK Phase 2 (CppBridge.initializeServices), which runs asynchronously after RunAnywhere.initialize(). The app calls GlobalState.markReady() without waiting for areServicesReady, so the UI can become usable before system TTS is in the registry and the TTS picker may omit or fail to load the system voice right after launch.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 009017d. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bugbot Autofix determined this is a false positive.

ModelBootstrap.setupModels already calls RunAnywhere.refreshModelRegistry(), which waits for Phase 2 services before GlobalState.markReady().

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

DetailRow("Total Tokens", "$totalTokens")
if (repliesWithContent > 0) {
DetailRow("Success Rate", "${stats.size * 100 / repliesWithContent}%")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Analytics success rate miscalculation

Low Severity

The "Success Rate" row divides the count of assistant messages that have stats by replies with non-empty text. Those sets differ, so the percentage can exceed 100% or imply failures when replies simply lack analytics, mislabeling performance data in the new chat analytics sheet.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 009017d. Configure here.

throw e
} catch (e: Throwable) {
RACLog.e("SDK setup failed", e)
GlobalState.markInitFailed(e.message ?: e.javaClass.simpleName)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Retry allows parallel setup

Medium Severity

retrySdkSetup() launches another runSdkSetup() coroutine with no guard if initialization is already in progress. Repeated retries (or retry during the first boot job) can overlap setupSDK() calls, including duplicate RunAnywhere.initialize() and backend registration work.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 009017d. Configure here.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cbd9998c-96c9-4448-a30f-9fc2197eb1eb

📥 Commits

Reviewing files that changed from the base of the PR and between 1e115b0 and 1b6953b.

📒 Files selected for processing (49)
  • examples/flutter/RunAnywhereAI/lib/app/content_view.dart
  • examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_status_components.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_types.dart
  • examples/flutter/RunAnywhereAI/lib/features/more/more_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart
  • examples/react-native/RunAnywhereAI/App.tsx
  • examples/react-native/RunAnywhereAI/README.md
  • examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx
  • examples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsx
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.ts
  • examples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx
  • examples/react-native/RunAnywhereAI/src/services/VLMService.ts
  • examples/react-native/RunAnywhereAI/src/types/index.ts
  • examples/react-native/RunAnywhereAI/src/types/settings.ts
  • examples/react-native/RunAnywhereAI/src/utils/loraFixture.ts
  • examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts
  • examples/web/RunAnywhereAI/src/components/model-selection.ts
  • examples/web/RunAnywhereAI/src/main.ts
  • examples/web/RunAnywhereAI/src/services/model-catalog.ts
  • examples/web/RunAnywhereAI/src/views/solutions.ts
  • examples/web/RunAnywhereAI/src/views/vision.ts
  • 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/Public/Extensions/LLM/RunAnywhere+TextGeneration.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/EventBus.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/RACErrors.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/SDKException.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts
 __________________________________
< Pulling the bugs out of the hat. >
 ----------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
📝 Walkthrough

Walkthrough

The PR updates app startup, media, and model flows across Android, iOS, Flutter, React Native, Kotlin, Swift, and commons, adding retryable initialization, new sample screens, SDK-owned audio paths, event/runtime API changes, model registry updates, and RAG/STT validation and bridge wiring.

Changes

Example apps and SDK runtime changes

Layer / File(s) Summary
Android startup orchestration, model bootstrap, and benchmark flow
examples/android/RunAnywhereAI/app/src/main/java/.../RunAnywhereApplication.kt, .../data/ModelBootstrap.kt, .../data/ModelCatalog.kt, .../data/benchmark/BenchmarkRunner.kt
Android startup now supports retryable SDK setup, backend registration ordering changed, catalog seeding re-registers models each launch, and benchmark execution/load-unload metric collection was reworked.
Android conversation contracts, search, and chat analytics
examples/android/RunAnywhereAI/app/src/main/java/.../data/conversation/*, .../ui/screens/chat/*
Conversation models and repository logic now store richer analytics, support search previews and smart titles, and wire chat analytics into top-bar, history, and details UI.
Android shell, settings, Tools, VAD, and Solutions screens
examples/android/RunAnywhereAI/app/src/main/java/.../data/settings/*, .../state/GlobalState.kt, .../ui/navigation/*, .../ui/screens/{intro,more,models,system_ui,tools,vad,solutions}/*, .../ui/theme/icons/RACIcons.kt
Settings and init error state were extended, scaffold/navigation gained new routes, and Tools, VAD, and Solutions feature screens and supporting UI state were added.
Android STT, TTS, and LoRA operational changes
examples/android/RunAnywhereAI/app/src/main/java/.../ui/screens/{stt,tts,lora}/*
Hybrid STT now passes raw audio, TTS uses SDK speaking for all voices, and LoRA adapter path handling now relies on stored local paths instead of filesystem scanning.
iOS sample catalog, chat analytics, hybrid STT, and voice cleanup
examples/ios/RunAnywhereAI/RunAnywhereAI/.../ModelCatalogBootstrap.swift, .../LLMViewModel*.swift, .../STTViewModel.swift, .../SpeechToTextView.swift, .../VoiceAgentViewModel.swift, .../RunAnywhereAIApp.swift
The iOS sample adds LoRA catalog seeding, TTFT-aware chat analytics, hybrid STT mode and UI, Apple device-state setup, and explicit voice-agent cleanup on stop and teardown.
Commons IDL, init events, validation, and hybrid audio normalization
idl/*, sdk/runanywhere-commons/include/..., sdk/runanywhere-commons/src/{features/jni/core/infrastructure/lifecycle/router}/*, sdk/runanywhere-commons/tests/*
IDL validation/default metadata was updated, commons now validates RAG configuration, initialization events moved to Phase 1 with duration payloads, JNI helpers were added, and hybrid STT proto paths normalize PCM16 to WAV.
Kotlin SDK core init, bridge lifecycle, transport, errors, and logging
sdk/runanywhere-kotlin/build.gradle.kts, sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/{foundation,httptransport,infrastructure,native,public}/...
Kotlin SDK init now wires native state and SDK events differently, telemetry bridge responsibilities shifted, HTTP transport/error mapping changed, exception metadata expanded, and logging/runtime configuration behavior was updated.
Kotlin SDK generation, model, storage, RAG, VAD, and voice APIs
sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/{Audio,LLM,Models,RAG,Storage,VAD,VoiceAgent}/*, .../hybrid/*, .../public/events/EventBus.kt
Kotlin public APIs were refactored across structured output, tool calling, model factories and registry, downloads, RAG, VAD, voice-agent behavior, and typed helpers/event subscriptions.
Swift SDK event, hybrid, audio, device, and RAG helpers
sdk/runanywhere-swift/Sources/RunAnywhere/{Foundation,Hybrid,Public}/..., sdk/shared/proto-ts/src/{convenience/rag_convenience.ts,rag.ts}
Swift runtime now starts the native event bus during init, adds payload/category event publishers, normalizes hybrid STT PCM input, adds PCM16-to-WAV and Apple device-state helpers, and extends RAG helper coverage.
Flutter SDK runtime, bindings, environment, and logging refactor
sdk/runanywhere-flutter/AGENTS.md, sdk/runanywhere-flutter/packages/runanywhere/lib/{core,foundation,native,public/configuration,public/extensions/...}, sdk/runanywhere-flutter/packages/runanywhere_{genie,llamacpp,onnx}/lib/*
Flutter runtime moved logger configuration into shared infrastructure, added native enum/version helpers, refactored blocking LLM calls onto isolates, updated environment/category defaults, and removed module metadata protocol usage.
Flutter SDK capabilities, media ownership, and public surface changes
sdk/runanywhere-flutter/packages/runanywhere/lib/{features,native,public/capabilities,public/events,public/extensions,public/hybrid,public/runanywhere.dart,runanywhere.dart}, sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml
Flutter SDK now owns audio capture and playback helpers, reworks downloads/events/media/model APIs, reduces several convenience surfaces, and broadens public exports.
Flutter example app migration to SDK-owned media
examples/flutter/RunAnywhereAI/lib/features/voice/*, examples/flutter/RunAnywhereAI/lib/core/services/*, examples/flutter/RunAnywhereAI/pubspec.yaml
The Flutter sample removed local audio services and switched STT and TTS flows to SDK-owned capture, playback, and playback-state streams.
React Native native core init, streaming, audio, and adapter infrastructure
sdk/runanywhere-react-native/packages/core/{cpp,ios,android}/..., sdk/runanywhere-react-native/packages/core/src/Adapters/*, sdk/runanywhere-react-native/packages/core/nitro.json
React Native native/core layers now return proto init results, add rc-to-proto error mapping, support host cloud STT providers and STT sessions, introduce generic stream fan-out, and add native audio capture/playback modules.
React Native public APIs, readiness guards, and error/event surfaces
sdk/runanywhere-react-native/packages/core/src/{Foundation,Public}/..., sdk/runanywhere-react-native/packages/core/src/index.ts, .../specs/RunAnywhereCore.nitro.ts, .../tests/unit/*
React Native public APIs gained typed event/model lifecycle streams, embeddings, audio conversion, cloud provider registration, archive and streaming model APIs, RAG helpers, and broader readiness/error handling changes.
React Native example migration to SDK-owned audio and downloads
examples/react-native/RunAnywhereAI/{src/*,ios/*,package.json,react-native.config.js,knip.json}, sdk/runanywhere-react-native/packages/core/README.md
The React Native sample removed native system-audio modules and system TTS handling, switched STT to SDK capture/transcription and TTS to SDK-only speech, and updated project/configuration/docs for SDK-owned audio modules.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • Flutter Sample #308 — Backend registration ordering and model-registry refresh/seeding changes touch the same initialization and model lookup paths.

Possibly related PRs

Suggested labels

kotlin-sample, kotlin-sdk, android-sample

Poem

🐇 I thumped through code from app to bridge,
and carried WAVs across each ridge.
New routes now hop, old paths grow lean,
with logs and models crisp and clean.
I nibble bugs, then grin and say:
“What a burrow-bright release today!”

✨ 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 feat/v2-architecture

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major 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 (3)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/benchmark/BenchmarkRunner.kt (1)

116-148: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail the benchmark when the LLM run times out.

withTimeoutOrNull returns null here, but the function still returns metrics and run() records the scenario as success = true. A timed-out stream therefore shows up as a successful benchmark with partial or empty stats. Throw on timeout, or when no final event arrives, before building the result.

Suggested fix
-            withTimeoutOrNull(BENCH_TIMEOUT) {
+            val completed = withTimeoutOrNull(BENCH_TIMEOUT) {
                 RunAnywhere.generateStream(
                     LLM_PROMPT,
                     RALLMGenerationOptions(max_tokens = maxTokens, temperature = 0f, system_prompt = LLM_SYSTEM_PROMPT),
                 ).collect { event ->
                     if (event.is_final) {
                         final = event.result
                         return@collect
                     }
                     if (event.token.isNotEmpty()) {
                         if (firstTokenNs == null) firstTokenNs = System.nanoTime()
                         tokens++
                     }
                 }
+                true
             }
+            if (completed != true || final == null) {
+                throw IllegalStateException("LLM benchmark timed out before receiving the final result")
+            }
             val e2eMs = (System.nanoTime() - start) / 1_000_000.0
             val r = final
🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/benchmark/BenchmarkRunner.kt`
around lines 116 - 148, The timeout path currently swallows a timed-out stream
(withTimeoutOrNull) and proceeds to return success metrics even when no final
event arrived; update the post-stream logic in BenchmarkRunner (the block using
withTimeoutOrNull(BENCH_TIMEOUT) and RunAnywhere.generateStream) to detect
timeout/no final result (e.g., if the final result variable is null or the
withTimeoutOrNull returned null) and throw an exception (or return a failed
result) before building BenchmarkMetrics so the benchmark is marked as failed;
ensure you check the same condition used to compute endToEndLatencyMs/ttftMs
(the final/result variable and firstTokenNs) and fail early if missing.
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt (1)

219-227: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin the Silero artifact to an immutable URL.

size_bytes now matches the current raw/master file exactly, but that URL is mutable. The next upstream model update will make this post-download size guard fail again on a valid download. Point this entry at a release asset or commit-pinned blob before relying on an exact byte count.

🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt`
around lines 219 - 227, The Silero VAD model entry (the "silero-vad" record in
ModelCatalog) uses a mutable raw/master URL which can change and will break the
exact post-download size guard; update the URL value to a commit-pinned or
release-asset blob URL (one that includes a specific tag or commit SHA) instead
of the raw/master path so the artifact is immutable and the 2_327_524 size_bytes
remains correct; locate the "silero-vad" entry in ModelCatalog (the
constructor/initializer that lists the model id, display name, URL, ONNX type,
ModelCategory.MODEL_CATEGORY_VOICE_ACTIVITY_DETECTION, and size_bytes) and
replace the URL string with the release/commit-pinned URL for that onnx file.
sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/RunAnywhere.kt (1)

601-608: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wait for Phase 2 to stop before tearing down the bridge.

servicesInitJob?.cancel() only requests cancellation. completeServicesInitialization() can still be inside initializePlatformBridgeServices() or CppBridgeSdkInit.phase2() when shutdownPlatformBridge() / CppBridgeState.shutdown() run, so reset() can race an in-flight JNI init against teardown and leave native state half-reset.

Suggested fix
+import kotlinx.coroutines.cancelAndJoin
+
 suspend fun reset() {
     logger.info("Resetting SDK state...")
 
-    synchronized(lock) {
-        servicesInitJob?.cancel()
-        servicesInitJob = null
+    val jobToCancel =
+        synchronized(lock) {
+            val job = servicesInitJob
+            servicesInitJob = null
+            job
+        }
+
+    jobToCancel?.cancelAndJoin()
 
+    synchronized(lock) {
             // Shutdown CppBridge, then clear persisted C++ state + auth.
             // Mirrors Swift reset(): CppBridge.shutdown() → CppBridge.State.shutdown().
             shutdownPlatformBridge()
             CppBridgeState.shutdown()
 
             _isInitialized = false
             _areServicesReady = false
             _hasCompletedHTTPSetup = false
             _httpSetupApplicable = true
             _currentEnvironment = null
             _initParams = null
-    }
+        }
 
     logger.info("SDK state reset completed")
 }
🤖 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-kotlin/src/main/kotlin/com/runanywhere/sdk/public/RunAnywhere.kt`
around lines 601 - 608, The reset synchronization currently cancels
servicesInitJob but proceeds to shutdownPlatformBridge() and
CppBridgeState.shutdown() immediately, which can race with an in-flight
completeServicesInitialization()/initializePlatformBridgeServices()/CppBridgeSdkInit.phase2();
after cancelling servicesInitJob you must wait for phase 2 to finish before
tearing down the bridge—call servicesInitJob?.cancelAndJoin() or otherwise await
servicesInitJob completion (or explicitly await
completeServicesInitialization()) inside the synchronized(lock) block before
invoking shutdownPlatformBridge() and CppBridgeState.shutdown().
🟡 Minor comments (5)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsViewModel.kt-28-29 (1)

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

Race condition on isRunning guard.

The check-then-set pattern is not atomic. If a user rapidly taps both "Voice Agent" and "RAG" buttons (or the same button twice), both invocations may see isRunning == false before either sets it to true, allowing concurrent solution runs.

Use an atomic guard or a mutex to prevent this race. For example, use AtomicBoolean.compareAndSet() or wrap the check-and-launch in a synchronized block.

🔒 Proposed fix using AtomicBoolean
+import java.util.concurrent.atomic.AtomicBoolean
+
 class SolutionsViewModel : ViewModel() {

     val log = mutableStateListOf<String>()

-    var isRunning by mutableStateOf(false)
-        private set
+    private val _isRunning = AtomicBoolean(false)
+    var isRunning by mutableStateOf(false)
+        private set

     fun runSolution(name: String, yaml: String) {
-        if (isRunning) return
-        isRunning = true
+        if (!_isRunning.compareAndSet(false, true)) return
+        isRunning = true
         log.add("→ $name: creating solution from YAML…")
         viewModelScope.launch {
             var handle: SolutionHandle? = null
             try {
                 handle = RunAnywhere.solutions.run(yaml)
                 log.add("✓ $name: handle created. Calling start()…")
                 handle.start()
                 log.add("✓ $name: started. Tearing down (demo).")
                 handle.destroy()
                 log.add("✓ $name: destroyed.")
             } catch (e: CancellationException) {
                 throw e
             } catch (e: Exception) {
                 log.add("✗ $name: ${e.message ?: e.toString()}")
             } finally {
                 withContext(NonCancellable) {
                     handle?.takeIf { it.isAlive }?.destroy()
                 }
+                _isRunning.set(false)
                 isRunning = 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
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsViewModel.kt`
around lines 28 - 29, The current non-atomic check-then-set on the isRunning
boolean in SolutionsViewModel causes a race; replace the boolean guard with an
atomic or mutex-based guard. Specifically, change the isRunning field to an
AtomicBoolean and replace the pattern "if (isRunning) return; isRunning = true"
with a compareAndSet(false, true) check (return if it fails), and ensure you
reset the atomic to false in a finally block after the run completes;
alternatively wrap the check-and-launch in a synchronized/mutex section around
the methods that start runs (the methods containing the current isRunning guard)
to ensure only one concurrent invocation.
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.kt-56-58 (1)

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

Use a consistent denominator for AI message analytics.

Line 58 undercounts AI messages, and Line 75 can produce >100% success because numerator/denominator are based on different subsets.

Suggested fix
-            DetailRow("From AI", "$repliesWithContent")
+            DetailRow("From AI", "${assistantMessages.size}")
...
-                if (repliesWithContent > 0) {
-                    DetailRow("Success Rate", "${stats.size * 100 / repliesWithContent}%")
+                if (assistantMessages.isNotEmpty()) {
+                    DetailRow(
+                        "Success Rate",
+                        "${repliesWithContent * 100 / assistantMessages.size}%",
+                    )
                 }

Also applies to: 74-76

🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.kt`
around lines 56 - 58, The AI-message analytics use inconsistent denominators:
compute a single aiMessagesCount = messages.count { !it.isUser } (or filter to
aiMessages = messages.filter { !it.isUser }) and then base all AI-related
metrics on that same set; update the DetailRow calls that currently use
messages.size and repliesWithContent to use aiMessagesCount and compute percent
as (if aiMessagesCount > 0) repliesWithContent * 100 / aiMessagesCount else 0 so
the raw count and percentage share the same denominator. Ensure you update any
percent display logic (where repliesWithContentPercent is shown) to use
aiMessagesCount as well.
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tools/ToolsScreen.kt-146-168 (1)

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

Wrap tool parameters instead of forcing one horizontal row.

This Row will clip or push content off-screen as soon as a tool exposes several parameters or moderately long names. On phones, that makes part of the schema unreadable. Use a wrapping layout such as FlowRow here so every parameter chip remains visible.

🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tools/ToolsScreen.kt`
around lines 146 - 168, Replace the horizontal Row that renders tool.parameters
with a wrapping FlowRow so parameter chips don’t get clipped on narrow screens:
swap the Row block around tool.parameters.forEach with a FlowRow (e.g.,
FlowRow(...)) and use mainAxisSpacing and crossAxisSpacing set to
dimens.spacingXs (or spacingSm where appropriate) to space chips, remove
verticalAlignment/horizontalArrangement props and keep the existing Surface/Text
chip rendering inside; update imports for FlowRow and any alignment API (e.g.,
crossAxisAlignment or alignment) as needed to center chips visually.
sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/Audio/RAAudioConvert.swift-67-98 (1)

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

Consider validating the sampleRate parameter.

Since pcm16ToWav is a public API, callers could pass invalid values (negative or zero). While the function won't crash, it would produce a malformed WAV header with incorrect byteRate and metadata. Adding a simple precondition would improve API safety and match the validation pattern used elsewhere in the SDK.

🛡️ Suggested validation
 static func pcm16ToWav(_ int16Data: Data, sampleRate: Int) -> Data {
+    precondition(sampleRate > 0, "sampleRate must be positive, got \(sampleRate)")
     let pcmFormatTag = 1
🤖 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-swift/Sources/RunAnywhere/Public/Extensions/Audio/RAAudioConvert.swift`
around lines 67 - 98, Validate the sampleRate at the start of pcm16ToWav(_
int16Data: Data, sampleRate: Int) to ensure it is > 0 (and optionally within a
reasonable upper bound), and fail fast using the same pattern used elsewhere
(e.g., precondition or guard with preconditionFailure) so invalid inputs don't
produce malformed WAV headers; update the function to check sampleRate before
computing byteRate/blockAlign and include a clear error message referencing
sampleRate in the assertion.
sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Audio/RunAnywhereAudioConvert.kt-70-93 (1)

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

Consider validating the sampleRate parameter.

Since pcm16ToWav is a public API, callers could pass invalid values (negative or zero). While the function won't crash, it would produce a malformed WAV header with incorrect byteRate and metadata. Adding a simple guard would improve API safety.

🛡️ Suggested validation
 fun RunAnywhere.pcm16ToWav(int16Bytes: ByteArray, sampleRate: Int): ByteArray {
+    require(sampleRate > 0) { "sampleRate must be positive, got $sampleRate" }
     val pcmFormatTag: Short = 1
🤖 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-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Audio/RunAnywhereAudioConvert.kt`
around lines 70 - 93, Validate the sampleRate at the start of
RunAnywhere.pcm16ToWav to ensure callers cannot pass zero or negative values;
e.g., check sampleRate > 0 and throw an IllegalArgumentException (or use
require) with a clear message if invalid so byteRate and WAV header fields
(byteRate, sampleRate, etc.) are not computed from bad input. Also consider
adding a short comment above the check describing the precondition.
🧹 Nitpick comments (2)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ConversationHistorySheet.kt (1)

63-66: ConversationRepository.search() doesn’t do IO per keystroke, but it can still be CPU-heavy
In ConversationHistorySheet.kt (lines 63–66), ConversationRepository.search(query) runs on every recomposition while typing; however ConversationRepository.search() is synchronous and only filters the in-memory conversations/summaries (no File/readText/Dispatchers.IO). Disk/JSON IO happens in ConversationStore inside the suspend refresh()/loadAll() path instead.
Main remaining risk is CPU scalability: it iterates all conversations and can scan message text per query. Consider debouncing and/or memoizing (derivedStateOf/remember) the filtered results to avoid UI jank with large histories.

🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ConversationHistorySheet.kt`
around lines 63 - 66, ConversationRepository.search(query) is being called on
every recomposition which can be CPU-heavy; wrap the filtering in a
memoized/derived state so repeated recompositions while typing don't re-run the
full scan. In ConversationHistorySheet.kt, replace the direct call that sets val
filtered = ConversationRepository.search(query) with a
remember/derivedStateOf-backed computed value that depends on query (and the
conversation list source if available), and/or debounce the query input before
invoking ConversationRepository.search to avoid calling it per keystroke.
sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift (1)

391-393: ⚡ Quick win

Cross-SDK consistency: Kotlin lacks this sherpa registration guard.

The Swift HybridSTTRouter now includes a helpful pre-flight check (requireSherpaRegistered()) that validates the sherpa plugin is registered before attempting to create the offline service. This produces actionable error messages listing registered plugins and the missing prerequisite.

The Kotlin HybridSTTRouter (in sdk/runanywhere-kotlin/.../HybridSTTRouter.kt) has no equivalent guard, so Kotlin users who forget ONNX.register() will encounter a less helpful vtable lookup failure deeper in the stack.

Consider adding the same guard to Kotlin's HybridRouterBridgeAdapter.createService for consistency and improved DX.

Also applies to: 475-487

🤖 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-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift`
around lines 391 - 393, Add the same pre-flight sherpa registration guard to the
Kotlin side: inside HybridRouterBridgeAdapter.createService, before creating the
offline/hybrid service, check that the Sherpa/ONNX plugin is registered (mirror
the Swift requireSherpaRegistered behavior) and throw a clear, descriptive
exception if not; include the missing prerequisite (e.g., "ONNX.register()") and
list currently registered plugins to aid debugging so callers get an actionable
error instead of a vtable lookup failure.
🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt`:
- Around line 43-47: retrySdkSetup() can launch overlapping coroutines that
concurrently call runSdkSetup(); guard against concurrent runs by adding a
single-run gate (e.g., an AtomicBoolean flag or a Mutex) around runSdkSetup()
invocation: set the flag (or lock mutex) at start of retrySdkSetup()/before
calling runSdkSetup(), clear/unlock when runSdkSetup() completes or fails, and
skip launching a new coroutine if the flag indicates setup is already in
progress; reference and protect retrySdkSetup(), runSdkSetup(), and
GlobalState.clearInitError() so clearInitError() still runs but does not allow
concurrent runSdkSetup() executions.

In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/state/GlobalState.kt`:
- Around line 23-25: The markInitFailed(message: String) function currently sets
initError but leaves ready unchanged; update markInitFailed to also set ready =
false so the app state reflects failed initialization (e.g., inside
GlobalState.markInitFailed set initError = message and ready = false) to prevent
a stale "ready" state after failures.

In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt`:
- Around line 74-85: The SDK event subscription in ChatViewModel.kt is global
and allows unrelated LLM events to overwrite activeGenerationTTFTMs /
activeGenerationMetrics; scope analytics to the active send by recording the
active operation/session id when a generation is started (the send method that
sets up the generation) and update handleGenerationEvent to ignore events whose
event.operation or session id does not match that active id; only merge cached
metrics into GenerationStats when the active id matches (the points where
metrics are persisted around the methods referenced at lines ~192-201 and
~244-250), and clear/reset the active id and cached metrics when the generation
completes or is cancelled so subsequent SDK events are ignored until a new
active id is set.

In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt`:
- Line 65: The YAML currently hard-codes vector_store_path:
"/tmp/ra-rag.usearch" which is invalid on Android; update the code that produces
or returns the YAML in SolutionsYaml (SolutionsYaml.kt) to construct the
vector_store_path at runtime using an app-specific directory (e.g.,
context.cacheDir.absolutePath or context.filesDir.absolutePath) or expose the
path as a configurable parameter so the SDK receives something like
"<cacheDir>/ra-rag.usearch" instead of "/tmp/...". Ensure the logic that builds
or returns the YAML string inserts that runtime path (or a passed-in path)
before handing it to the SDK.

In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vad/VadScreen.kt`:
- Around line 73-77: The current guard in onListen checks `model == null` and
prevents calling `vadVm.toggle()` which blocks the user from stopping
mid-session if the model disappears; change the logic so that you only prevent
starting listening when no model is available but still allow stopping: call
`vadVm.toggle()` if the VAD is currently active (e.g., check
`vadVm.isListening()` or the VM's running state) or if `model != null` for
starting; keep the permission check and
`permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)` for start attempts
without permission. Update the same pattern in the other handler (the block
referenced at lines ~99-103) so stop actions are never blocked by `model ==
null`.

In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vad/VadViewModel.kt`:
- Around line 60-63: The VAD audio channel is created as Channel.UNLIMITED which
can accumulate chunks from recorder.start { ... } and OOM if
RunAnywhere.streamVAD(...) is slow; change the channel used for audio to a
bounded channel (e.g., Channel.BUFFERED or Channel(capacity = N) with a
reasonable small N) and handle backpressure by dropping or suspending senders
(use trySend and check result or use send within a coroutine) so
recorder.start's audio?.trySend(RunAnywhere.pcm16ToFloat32(chunk)) no longer
grows unbounded; update the code where the channel is constructed (the audio
declaration) and adjust consumers (RunAnywhere.streamVAD / any collectors) to
handle dropped/failed sends accordingly.

In
`@examples/ios/RunAnywhereAI/RunAnywhereAI/Core/Services/ModelCatalogBootstrap.swift`:
- Around line 226-229: The LoRA bootstrap currently registers only one adapter;
update the registerLoraAdapters flow (and any helper used there) to register
five curated LoRA entries via LoRAAdapterCatalog.registerAll(), ensuring each
entry supports downloading to ~/Documents/LoRA/, validates the file magic byte
0x47475546 before accepting, and wires application/removal to
RunAnywhere.lora.apply(...) and RunAnywhere.lora.remove(...); implement the same
five-entry seeding wherever registerLoraAdapters is called so the startup
contract (5 curated entries, download support, magic-byte validation,
apply/remove integration) is fully satisfied.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.kt`:
- Around line 49-56: The production LoggingConfiguration currently disables
local and Sentry logging (enable_local_logging = false, enable_sentry_logging =
false) which, combined with the proto default for enable_remote_logging, causes
Logging.log to return early and drop warnings/errors; update the production
preset in SDKLogger.kt (the val production LoggingConfiguration) to enable a
fallback sink so warning+ messages aren't discarded — for example set
enable_local_logging = true (or set enable_remote_logging = true if you prefer
remote delivery) so Logging.log reaches commonsLogBridge and emits
warnings/errors; no changes needed to Logging.log itself.
- Around line 121-134: The current direct write to _configuration in the
Logging.configuration setter bypasses Sentry transitions; update the property
setter to route every config change through the existing configure(config:
LoggingConfiguration) method (or invoke the same
sentrySetupHook/sentryTeardownHook logic) instead of assigning _configuration
directly, so that configure(), sentrySetupHook, and sentryTeardownHook stay in
sync with any changes made via the Logging.configuration property.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt`:
- Around line 1373-1385: The current racApiErrorFromResponse external bridging
method returns a positional String[3] which is brittle; change its contract to
return serialized SDKError bytes (e.g., ByteArray?) instead of Array<String?>?
so JNI/C++ can return a canonical proto SDKError payload; update the declaration
of racApiErrorFromResponse in RunAnywhereBridge.kt and the corresponding
native/JNI implementation to serialize/deserialize SDKError proto bytes, and
update callers to parse the bytes into the SDKError proto and throw or wrap into
SDKException (using SDKError.code/category/message/c_abi_code) rather than
reconstructing error state from string positions.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereLoRA.kt`:
- Around line 288-299: The catch in checkCompatibility currently swallows
CancellationException causing cancelled coroutines to be reported as
incompatible; update checkCompatibility (which calls ensureLoraReady() and
CppBridgeLoraRegistry.compatibility(config) inside withContext) to re-throw
coroutine cancellation (e.g., if e is CancellationException or
e.isCancellation()) before returning the fallback LoraCompatibilityResult so
that cancellations propagate correctly.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/ToolCallingOrchestrator.kt`:
- Around line 227-229: The code that builds the tool-calling session request
incorrectly always uses llmOpts.top_p, ignoring any ToolCallingOptions override;
update the top_p assignment in the request construction (in
ToolCallingOrchestrator.kt where max_tokens/temperature/top_p are set) to use
effectiveOpts.top_p ?: llmOpts.top_p so tool-level top_p values are honored
(follow the same field-by-field override pattern used for max_tokens and
temperature).

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/RAG/RunAnywhereRAG.kt`:
- Around line 191-198: The function RunAnywhere.ragGetDocumentCount currently
swallows all exceptions and returns 0, masking JNI/bridge failures; update
RunAnywhere.ragGetDocumentCount so it does not catch Exception broadly—either
remove the try/catch so exceptions from CppBridgeRAG.stats() propagate, or catch
only the specific known "no index" condition and return 0 in that case; ensure
CppBridgeRAG.stats() calls remain unchanged but that any unexpected failures are
rethrown (or wrapped) instead of being translated to 0 so callers can detect
bridge/JNI errors.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereDownload.kt`:
- Around line 264-269: The thrown exceptions after transfer completion
incorrectly use a network error shape; update the SDKException.make calls (the
ones that currently pass message "Download completed without a local_path;
cannot import completion into the model registry" and the other similar branch)
to use an internal/storage error shape instead of
ErrorCategory.ERROR_CATEGORY_NETWORK — use ErrorCategory.ERROR_CATEGORY_INTERNAL
or a storage-specific category if available and a storage-specific error code
(e.g., ERROR_CODE_INTERNAL_STORAGE_INVALID_STATE or a newly added storage
invalid-state code) while preserving the descriptive message and shouldLog flag;
apply this change to both SDKException.make occurrences in
RunAnywhereDownload.kt.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereStorage.kt`:
- Line 39: The JNI helper racModelIdFromUrl is being used directly in the public
API (RunAnywhereStorage) and its empty-string result is treated as a valid ID
via getOrNull() ?: name, which can persist blank model IDs; instead, create a
CppBridge-style extension (e.g., CppBridgeModelId or extension function on
RunAnywhereBridge like racModelIdFromUrlSafe) that calls the RunAnywhereBridge
JNI function and converts "" or all-whitespace to null (trim and isBlank check),
then update RunAnywhereStorage where you call racModelIdFromUrl/getOrNull() ?:
name and registerModel(id = ...) to use the new safe helper so blank strings
become null and the fallback/name logic is correct; replace any direct
RunAnywhereBridge usage in the public layer with calls to this CppBridge helper
(apply same fix for the other occurrences around lines 66-87 and 248-249).

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VoiceAgent/RunAnywhereVoiceAgent.kt`:
- Around line 249-264: The current implementation of
RunAnywhere.streamVoiceAgent returns early on isInitialized == false or when
CppBridgeVoiceAgent.getHandle() fails, producing a silent empty stream; instead,
on those failures emit a terminal VoiceEvent error event (don’t swallow the
exception) so callers see a .error. Change the early returns in
RunAnywhere.streamVoiceAgent: for CancellationException rethrow as before, and
for other failures (uninitialized or Throwable from
ensureServicesReady()/CppBridgeVoiceAgent.getHandle()) emit a VoiceEvent.Error
containing a clear message and the caught throwable details before completing;
only call VoiceAgentStreamAdapter(handle).stream() when a valid handle is
obtained. Use the existing types and functions (isInitialized,
ensureServicesReady(), CppBridgeVoiceAgent.getHandle(), VoiceEvent.Error,
VoiceAgentStreamAdapter.stream()) to implement this.
- Around line 185-197: When ensureVAD is true the code calls ensureDefaultVAD()
but ignores its boolean result, allowing initialization to continue even if VAD
failed; update the initialization flow in RunAnywhereVoiceAgent so that after
calling ensureDefaultVAD() you check its return value and if it returns false
throw the same SDKException (or a similar ErrorCode/ErrorCategory) before
proceeding to getMissingComponents(), ensuring a failure is raised early instead
of producing a partially initialized agent; reference ensureDefaultVAD(), the
ensureVAD flag, getMissingComponents(), and the existing SDKException.make call
to mirror the current error pattern.

---

Outside diff comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/benchmark/BenchmarkRunner.kt`:
- Around line 116-148: The timeout path currently swallows a timed-out stream
(withTimeoutOrNull) and proceeds to return success metrics even when no final
event arrived; update the post-stream logic in BenchmarkRunner (the block using
withTimeoutOrNull(BENCH_TIMEOUT) and RunAnywhere.generateStream) to detect
timeout/no final result (e.g., if the final result variable is null or the
withTimeoutOrNull returned null) and throw an exception (or return a failed
result) before building BenchmarkMetrics so the benchmark is marked as failed;
ensure you check the same condition used to compute endToEndLatencyMs/ttftMs
(the final/result variable and firstTokenNs) and fail early if missing.

In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt`:
- Around line 219-227: The Silero VAD model entry (the "silero-vad" record in
ModelCatalog) uses a mutable raw/master URL which can change and will break the
exact post-download size guard; update the URL value to a commit-pinned or
release-asset blob URL (one that includes a specific tag or commit SHA) instead
of the raw/master path so the artifact is immutable and the 2_327_524 size_bytes
remains correct; locate the "silero-vad" entry in ModelCatalog (the
constructor/initializer that lists the model id, display name, URL, ONNX type,
ModelCategory.MODEL_CATEGORY_VOICE_ACTIVITY_DETECTION, and size_bytes) and
replace the URL string with the release/commit-pinned URL for that onnx file.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/RunAnywhere.kt`:
- Around line 601-608: The reset synchronization currently cancels
servicesInitJob but proceeds to shutdownPlatformBridge() and
CppBridgeState.shutdown() immediately, which can race with an in-flight
completeServicesInitialization()/initializePlatformBridgeServices()/CppBridgeSdkInit.phase2();
after cancelling servicesInitJob you must wait for phase 2 to finish before
tearing down the bridge—call servicesInitJob?.cancelAndJoin() or otherwise await
servicesInitJob completion (or explicitly await
completeServicesInitialization()) inside the synchronized(lock) block before
invoking shutdownPlatformBridge() and CppBridgeState.shutdown().

---

Minor comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.kt`:
- Around line 56-58: The AI-message analytics use inconsistent denominators:
compute a single aiMessagesCount = messages.count { !it.isUser } (or filter to
aiMessages = messages.filter { !it.isUser }) and then base all AI-related
metrics on that same set; update the DetailRow calls that currently use
messages.size and repliesWithContent to use aiMessagesCount and compute percent
as (if aiMessagesCount > 0) repliesWithContent * 100 / aiMessagesCount else 0 so
the raw count and percentage share the same denominator. Ensure you update any
percent display logic (where repliesWithContentPercent is shown) to use
aiMessagesCount as well.

In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsViewModel.kt`:
- Around line 28-29: The current non-atomic check-then-set on the isRunning
boolean in SolutionsViewModel causes a race; replace the boolean guard with an
atomic or mutex-based guard. Specifically, change the isRunning field to an
AtomicBoolean and replace the pattern "if (isRunning) return; isRunning = true"
with a compareAndSet(false, true) check (return if it fails), and ensure you
reset the atomic to false in a finally block after the run completes;
alternatively wrap the check-and-launch in a synchronized/mutex section around
the methods that start runs (the methods containing the current isRunning guard)
to ensure only one concurrent invocation.

In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tools/ToolsScreen.kt`:
- Around line 146-168: Replace the horizontal Row that renders tool.parameters
with a wrapping FlowRow so parameter chips don’t get clipped on narrow screens:
swap the Row block around tool.parameters.forEach with a FlowRow (e.g.,
FlowRow(...)) and use mainAxisSpacing and crossAxisSpacing set to
dimens.spacingXs (or spacingSm where appropriate) to space chips, remove
verticalAlignment/horizontalArrangement props and keep the existing Surface/Text
chip rendering inside; update imports for FlowRow and any alignment API (e.g.,
crossAxisAlignment or alignment) as needed to center chips visually.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Audio/RunAnywhereAudioConvert.kt`:
- Around line 70-93: Validate the sampleRate at the start of
RunAnywhere.pcm16ToWav to ensure callers cannot pass zero or negative values;
e.g., check sampleRate > 0 and throw an IllegalArgumentException (or use
require) with a clear message if invalid so byteRate and WAV header fields
(byteRate, sampleRate, etc.) are not computed from bad input. Also consider
adding a short comment above the check describing the precondition.

In
`@sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/Audio/RAAudioConvert.swift`:
- Around line 67-98: Validate the sampleRate at the start of pcm16ToWav(_
int16Data: Data, sampleRate: Int) to ensure it is > 0 (and optionally within a
reasonable upper bound), and fail fast using the same pattern used elsewhere
(e.g., precondition or guard with preconditionFailure) so invalid inputs don't
produce malformed WAV headers; update the function to check sampleRate before
computing byteRate/blockAlign and include a clear error message referencing
sampleRate in the assertion.

---

Nitpick comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ConversationHistorySheet.kt`:
- Around line 63-66: ConversationRepository.search(query) is being called on
every recomposition which can be CPU-heavy; wrap the filtering in a
memoized/derived state so repeated recompositions while typing don't re-run the
full scan. In ConversationHistorySheet.kt, replace the direct call that sets val
filtered = ConversationRepository.search(query) with a
remember/derivedStateOf-backed computed value that depends on query (and the
conversation list source if available), and/or debounce the query input before
invoking ConversationRepository.search to avoid calling it per keystroke.

In `@sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift`:
- Around line 391-393: Add the same pre-flight sherpa registration guard to the
Kotlin side: inside HybridRouterBridgeAdapter.createService, before creating the
offline/hybrid service, check that the Sherpa/ONNX plugin is registered (mirror
the Swift requireSherpaRegistered behavior) and throw a clear, descriptive
exception if not; include the missing prerequisite (e.g., "ONNX.register()") and
list currently registered plugins to aid debugging so callers get an actionable
error instead of a vtable lookup failure.
🪄 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: 977e7ac0-5abc-40f5-a133-410dd273f020

📥 Commits

Reviewing files that changed from the base of the PR and between 526c649 and 009017d.

📒 Files selected for processing (61)
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelBootstrap.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/benchmark/BenchmarkRunner.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/conversation/ConversationModels.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/conversation/ConversationRepository.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/settings/AppSettings.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/settings/SettingsRepository.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/state/GlobalState.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/navigation/AppNavHost.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/navigation/Destinations.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatModels.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatTopBar.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ConversationHistorySheet.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/intro/IntroScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelSelectionContext.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/more/MoreScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/stt/SttViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/system_ui/AppScaffold.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/system_ui/AppTopBar.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tools/ToolsScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tools/ToolsViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vad/VadScreen.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vad/VadViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/icons/RACIcons.kt
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Core/Services/ModelCatalogBootstrap.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+Analytics.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+Generation.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/VoiceAgentViewModel.swift
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
  • sdk/runanywhere-kotlin/build.gradle.kts
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeAuth.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeState.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/httptransport/OkHttpHttpTransport.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/HybridSTTRouter.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/RunAnywhere.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Audio/RunAnywhereAudioConvert.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereLoRA.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereStructuredOutput.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereTextGeneration.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereToolCalling.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/StructuredOutputProtoHelpers.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/ToolCallingOrchestrator.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Models/RunAnywhereModelRegistry.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/RAG/RunAnywhereRAG.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereDownload.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereStorage.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VAD/RunAnywhereVAD.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VoiceAgent/RunAnywhereVoiceAgent.kt
  • sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Public/Events/EventBus.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/Audio/RAAudioConvert.swift

Comment on lines +43 to +47
fun retrySdkSetup() {
GlobalState.clearInitError()
appScope.launch(Dispatchers.IO) {
runSdkSetup()
}

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 | 🟠 Major | ⚡ Quick win

Prevent concurrent SDK setup runs.

retrySdkSetup() can start overlapping setup coroutines when retried rapidly, which may run backend registration/initialization concurrently and leave app init state nondeterministic.

Suggested fix
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 import kotlin.coroutines.cancellation.CancellationException

 class RunAnywhereApplication : Application() {

     private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+    private var sdkSetupJob: Job? = null
@@
-        appScope.launch(Dispatchers.IO) {
+        sdkSetupJob = appScope.launch(Dispatchers.IO) {
             ConversationRepository.refresh()
             runSdkSetup()
         }
@@
     fun retrySdkSetup() {
+        if (sdkSetupJob?.isActive == true) return
         GlobalState.clearInitError()
-        appScope.launch(Dispatchers.IO) {
+        sdkSetupJob = appScope.launch(Dispatchers.IO) {
             runSdkSetup()
         }
     }
🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt`
around lines 43 - 47, retrySdkSetup() can launch overlapping coroutines that
concurrently call runSdkSetup(); guard against concurrent runs by adding a
single-run gate (e.g., an AtomicBoolean flag or a Mutex) around runSdkSetup()
invocation: set the flag (or lock mutex) at start of retrySdkSetup()/before
calling runSdkSetup(), clear/unlock when runSdkSetup() completes or fails, and
skip launching a new coroutine if the flag indicates setup is already in
progress; reference and protect retrySdkSetup(), runSdkSetup(), and
GlobalState.clearInitError() so clearInitError() still runs but does not allow
concurrent runSdkSetup() executions.

Comment on lines +23 to +25
fun markInitFailed(message: String) {
initError = message
}

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 | 🟠 Major | ⚡ Quick win

Mark init failures as not-ready.

markInitFailed() sets initError but leaves ready untouched. If a later setup attempt fails after a prior success, app state can remain "ready" while initialization is actually failed.

Suggested fix
 fun markInitFailed(message: String) {
+    ready = false
     initError = message
 }
🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/state/GlobalState.kt`
around lines 23 - 25, The markInitFailed(message: String) function currently
sets initError but leaves ready unchanged; update markInitFailed to also set
ready = false so the app state reflects failed initialization (e.g., inside
GlobalState.markInitFailed set initError = message and ready = false) to prevent
a stale "ready" state after failures.

llm_model_id: "smollm2-360m-q8_0"

vector_store: "usearch"
vector_store_path: "/tmp/ra-rag.usearch"

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 | 🟠 Major | ⚡ Quick win

Hard-coded /tmp path will fail on Android.

The vector_store_path: "/tmp/ra-rag.usearch" path is not writable on Android. Android restricts file system access, and /tmp does not exist or is not accessible to apps.

Replace with an app-specific directory such as context.cacheDir or context.filesDir. Consider making the path configurable or constructing it at runtime before passing the YAML to the SDK.

🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt`
at line 65, The YAML currently hard-codes vector_store_path:
"/tmp/ra-rag.usearch" which is invalid on Android; update the code that produces
or returns the YAML in SolutionsYaml (SolutionsYaml.kt) to construct the
vector_store_path at runtime using an app-specific directory (e.g.,
context.cacheDir.absolutePath or context.filesDir.absolutePath) or expose the
path as a configurable parameter so the SDK receives something like
"<cacheDir>/ra-rag.usearch" instead of "/tmp/...". Ensure the logic that builds
or returns the YAML string inserts that runtime path (or a passed-in path)
before handing it to the SDK.

Comment on lines +73 to +77
fun onListen() {
if (model == null) return
val granted = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
if (granted) vadVm.toggle() else permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)

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 | 🟠 Major | ⚡ Quick win

Keep the stop action enabled even when no model is currently resolved.

Line 74 and Line 101 together block toggle() when model == null; if a model disappears mid-session, the user can’t manually stop listening.

Suggested fix
     fun onListen() {
-        if (model == null) return
+        if (vadVm.isListening) {
+            vadVm.toggle()
+            return
+        }
+        if (model == null) return
         val granted = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
             PackageManager.PERMISSION_GRANTED
         if (granted) vadVm.toggle() else permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
     }
@@
         ListenButton(
             listening = vadVm.isListening,
-            enabled = model != null,
+            enabled = vadVm.isListening || model != null,
             onClick = ::onListen,
         )

Also applies to: 99-103

🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vad/VadScreen.kt`
around lines 73 - 77, The current guard in onListen checks `model == null` and
prevents calling `vadVm.toggle()` which blocks the user from stopping
mid-session if the model disappears; change the logic so that you only prevent
starting listening when no model is available but still allow stopping: call
`vadVm.toggle()` if the VAD is currently active (e.g., check
`vadVm.isListening()` or the VM's running state) or if `model != null` for
starting; keep the permission check and
`permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)` for start attempts
without permission. Update the same pattern in the other handler (the block
referenced at lines ~99-103) so stop actions are never blocked by `model ==
null`.

Comment on lines 191 to 198
suspend fun RunAnywhere.ragGetDocumentCount(): Int =
withContext(Dispatchers.IO) {
ensureServicesReady()
CppBridgeRAG.stats().indexed_chunks.toInt()
try {
CppBridgeRAG.stats().indexed_chunks.toInt()
} catch (_: Exception) {
0
}
}

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 | 🟠 Major | ⚡ Quick win

Don't mask RAG stats failures as an empty index.

ragGetDocumentCount() now returns 0 for every thrown Exception, so bridge/JNI failures become indistinguishable from a genuinely empty corpus. That makes callers treat a broken pipeline as healthy.

Proposed fix
-suspend fun RunAnywhere.ragGetDocumentCount(): Int =
-    withContext(Dispatchers.IO) {
-        try {
-            CppBridgeRAG.stats().indexed_chunks.toInt()
-        } catch (_: Exception) {
-            0
-        }
-    }
+suspend fun RunAnywhere.ragGetDocumentCount(): Int {
+    if (!isInitialized) return 0
+    ensureServicesReady()
+    return withContext(Dispatchers.IO) {
+        CppBridgeRAG.stats().indexed_chunks.toInt()
+    }
+}
🤖 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-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/RAG/RunAnywhereRAG.kt`
around lines 191 - 198, The function RunAnywhere.ragGetDocumentCount currently
swallows all exceptions and returns 0, masking JNI/bridge failures; update
RunAnywhere.ragGetDocumentCount so it does not catch Exception broadly—either
remove the try/catch so exceptions from CppBridgeRAG.stats() propagate, or catch
only the specific known "no index" condition and return 0 in that case; ensure
CppBridgeRAG.stats() calls remain unchanged but that any unexpected failures are
rethrown (or wrapped) instead of being translated to 0 so callers can detect
bridge/JNI errors.

Comment on lines +264 to 269
throw SDKException.make(
code = ErrorCode.ERROR_CODE_INVALID_STATE,
message = "Download completed without a local_path; cannot import completion into the model registry",
category = ErrorCategory.ERROR_CATEGORY_NETWORK,
shouldLog = false,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't report post-download registry failures as network download failures.

Both of these branches run after the transfer has already completed. Throwing a network/download error here hides a local registry-sync problem and encourages the wrong retry path, even though the bytes are already on disk. Please use an internal/storage/invalid-state error shape for these paths instead.

Also applies to: 288-295

🤖 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-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereDownload.kt`
around lines 264 - 269, The thrown exceptions after transfer completion
incorrectly use a network error shape; update the SDKException.make calls (the
ones that currently pass message "Download completed without a local_path;
cannot import completion into the model registry" and the other similar branch)
to use an internal/storage error shape instead of
ErrorCategory.ERROR_CATEGORY_NETWORK — use ErrorCategory.ERROR_CATEGORY_INTERNAL
or a storage-specific category if available and a storage-specific error code
(e.g., ERROR_CODE_INTERNAL_STORAGE_INVALID_STATE or a newly added storage
invalid-state code) while preserving the descriptive message and shouldLog flag;
apply this change to both SDKException.make occurrences in
RunAnywhereDownload.kt.

import com.runanywhere.sdk.foundation.bridge.extensions.CppBridgeModelRegistry
import com.runanywhere.sdk.foundation.bridge.extensions.CppBridgeStorage
import com.runanywhere.sdk.foundation.errors.SDKException
import com.runanywhere.sdk.native.bridge.RunAnywhereBridge

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 | 🟠 Major | ⚡ Quick win

Don't persist blank IDs from the native model-id helper.

getOrNull() ?: name treats "" from racModelIdFromUrl() as a valid ID, so registerModel(id = null, ...) can save an empty model.id for URLs without a basename. That breaks later registry lookups/downloads on the new storage registration path. Please normalize blank output before falling back, and keep this JNI call behind a CppBridge* helper instead of reaching into RunAnywhereBridge from the public API layer. As per coding guidelines, "JNI bridge architecture must follow the pattern: Kotlin code → CppBridge* extension objects (type conversion, error mapping) → RunAnywhereBridge external fun declarations (JNI boundary) → librac_commons.so C functions."

Also applies to: 66-87, 248-249

🤖 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-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereStorage.kt`
at line 39, The JNI helper racModelIdFromUrl is being used directly in the
public API (RunAnywhereStorage) and its empty-string result is treated as a
valid ID via getOrNull() ?: name, which can persist blank model IDs; instead,
create a CppBridge-style extension (e.g., CppBridgeModelId or extension function
on RunAnywhereBridge like racModelIdFromUrlSafe) that calls the
RunAnywhereBridge JNI function and converts "" or all-whitespace to null (trim
and isBlank check), then update RunAnywhereStorage where you call
racModelIdFromUrl/getOrNull() ?: name and registerModel(id = ...) to use the new
safe helper so blank strings become null and the fallback/name logic is correct;
replace any direct RunAnywhereBridge usage in the public layer with calls to
this CppBridge helper (apply same fix for the other occurrences around lines
66-87 and 248-249).

Source: Coding guidelines

Comment on lines 185 to 197
if (ensureVAD) {
ensureDefaultVAD()
}

if (voiceAgentInitialized && areAllComponentsLoaded()) return
if (!areAllComponentsLoaded()) {
val missing = getMissingComponents()
throw SDKException.voiceAgent("Cannot initialize: Models not loaded: ${missing.joinToString(", ")}")
val missing = getMissingComponents()
if (missing.isNotEmpty()) {
throw SDKException.make(
code = ErrorCode.ERROR_CODE_MODEL_NOT_LOADED,
message = "Cannot initialize voice agent: Models not loaded: ${missing.joinToString(", ")}",
category = ErrorCategory.ERROR_CATEGORY_COMPONENT,
shouldLog = false,
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor ensureVAD failure before continuing initialization.

When ensureVAD is true, ensureDefaultVAD() returning false means no VAD model was loaded, but initialization still continues and the later missing-model check only covers STT/LLM/TTS. That violates this method's own contract and can produce a "ready" voice agent that never emits the lifecycle events it depends on.

Proposed fix
-    if (ensureVAD) {
-        ensureDefaultVAD()
-    }
+    if (ensureVAD && !ensureDefaultVAD()) {
+        throw SDKException.make(
+            code = ErrorCode.ERROR_CODE_MODEL_NOT_LOADED,
+            message = "Cannot initialize voice agent: VAD model not loaded",
+            category = ErrorCategory.ERROR_CATEGORY_COMPONENT,
+            shouldLog = false,
+        )
+    }
📝 Committable suggestion

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

Suggested change
if (ensureVAD) {
ensureDefaultVAD()
}
if (voiceAgentInitialized && areAllComponentsLoaded()) return
if (!areAllComponentsLoaded()) {
val missing = getMissingComponents()
throw SDKException.voiceAgent("Cannot initialize: Models not loaded: ${missing.joinToString(", ")}")
val missing = getMissingComponents()
if (missing.isNotEmpty()) {
throw SDKException.make(
code = ErrorCode.ERROR_CODE_MODEL_NOT_LOADED,
message = "Cannot initialize voice agent: Models not loaded: ${missing.joinToString(", ")}",
category = ErrorCategory.ERROR_CATEGORY_COMPONENT,
shouldLog = false,
)
}
if (ensureVAD && !ensureDefaultVAD()) {
throw SDKException.make(
code = ErrorCode.ERROR_CODE_MODEL_NOT_LOADED,
message = "Cannot initialize voice agent: VAD model not loaded",
category = ErrorCategory.ERROR_CATEGORY_COMPONENT,
shouldLog = false,
)
}
val missing = getMissingComponents()
if (missing.isNotEmpty()) {
throw SDKException.make(
code = ErrorCode.ERROR_CODE_MODEL_NOT_LOADED,
message = "Cannot initialize voice agent: Models not loaded: ${missing.joinToString(", ")}",
category = ErrorCategory.ERROR_CATEGORY_COMPONENT,
shouldLog = 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-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VoiceAgent/RunAnywhereVoiceAgent.kt`
around lines 185 - 197, When ensureVAD is true the code calls ensureDefaultVAD()
but ignores its boolean result, allowing initialization to continue even if VAD
failed; update the initialization flow in RunAnywhereVoiceAgent so that after
calling ensureDefaultVAD() you check its return value and if it returns false
throw the same SDKException (or a similar ErrorCode/ErrorCategory) before
proceeding to getMissingComponents(), ensuring a failure is raised early instead
of producing a partially initialized agent; reference ensureDefaultVAD(), the
ensureVAD flag, getMissingComponents(), and the existing SDKException.make call
to mirror the current error pattern.

Comment on lines +249 to 264
fun RunAnywhere.streamVoiceAgent(): Flow<VoiceEvent> =
flow {
if (!isInitialized) return@flow

val handle =
try {
ensureServicesReady()
CppBridgeVoiceAgent.getHandle()
} catch (cancellation: CancellationException) {
throw cancellation
} catch (_: Throwable) {
return@flow
}

emitAll(VoiceAgentStreamAdapter(handle).stream())
}

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 | 🟠 Major | ⚡ Quick win

Don't turn voice-agent startup failures into a silent empty stream.

Returning from the flow when the SDK is uninitialized or getHandle() fails gives callers neither an exception nor a terminal .error event, so the stream looks idle instead of failed. That makes recovery and user feedback impossible.

As per coding guidelines, streamVoiceAgent() must stream voice-agent events including .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
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VoiceAgent/RunAnywhereVoiceAgent.kt`
around lines 249 - 264, The current implementation of
RunAnywhere.streamVoiceAgent returns early on isInitialized == false or when
CppBridgeVoiceAgent.getHandle() fails, producing a silent empty stream; instead,
on those failures emit a terminal VoiceEvent error event (don’t swallow the
exception) so callers see a .error. Change the early returns in
RunAnywhere.streamVoiceAgent: for CancellationException rethrow as before, and
for other failures (uninitialized or Throwable from
ensureServicesReady()/CppBridgeVoiceAgent.getHandle()) emit a VoiceEvent.Error
containing a clear message and the caught throwable details before completing;
only call VoiceAgentStreamAdapter(handle).stream() when a valid handle is
obtained. Use the existing types and functions (isInitialized,
ensureServicesReady(), CppBridgeVoiceAgent.getHandle(), VoiceEvent.Error,
VoiceAgentStreamAdapter.stream()) to implement this.

Source: Coding guidelines

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

Caution

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

⚠️ Outside diff range comments (1)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt (1)

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

Tool-assisted generations lack analytics.

generateWithTools doesn't populate GenerationStats, unlike generateReply and streamReply. This means tool-calling messages won't have token counts, latency, or throughput metrics persisted or displayed in ChatDetailsSheet.

Consider extracting stats from result (it likely has tokens_generated, generation_time_ms, etc.) to maintain analytics consistency.

🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt`
around lines 243 - 274, generateWithTools currently updates message text/tool
info but does not populate GenerationStats, so tool-assisted messages lack
token/latency/throughput analytics; extract the relevant stats from result
(e.g., tokens_generated, tokens_input, generation_time_ms or similarly named
fields returned by RunAnywhere.generateWithTools), compute throughput if needed,
build a GenerationStats instance, and include it in the message update
(messages[index].copy(..., generationStats = yourStats)) analogous to how
generateReply/streamReply set stats so ChatDetailsSheet can display token counts
and latency.
🧹 Nitpick comments (1)
examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/SpeechToTextView.swift (1)

334-361: 💤 Low value

Missing UI control for hybridMinBattery setting.

The ViewModel exposes hybridMinBattery (line 40 in STTViewModel) which is used in routing policy (line 350), but there's no corresponding UI control in this configuration section. Users cannot adjust the minimum battery threshold for hybrid routing.

Consider adding a slider similar to the confidence threshold:

VStack(alignment: .leading, spacing: 4) {
    Text("Min battery \(Int(viewModel.hybridMinBattery))%")
        .font(.caption)
        .foregroundColor(.secondary)
    Slider(value: $viewModel.hybridMinBattery, in: 0...100, step: 5)
}
🤖 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/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/SpeechToTextView.swift`
around lines 334 - 361, Add a UI control to hybridConfigurationSection so users
can adjust viewModel.hybridMinBattery: insert a small VStack (like the existing
fallback threshold block for hybridConfidenceThreshold) that displays "Min
battery \(Int(viewModel.hybridMinBattery))%" and binds a Slider to
$viewModel.hybridMinBattery with range 0...100 and an appropriate step (e.g.,
5), matching the styling (font .caption, foregroundColor .secondary) and
placement within the hybridConfigurationSection so the hybridMinBattery setting
is editable alongside hybridConfidenceThreshold.
🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/lora/LoraViewModel.kt`:
- Around line 126-131: Change isDownloaded to reflect the actual usable local
path instead of relying on entry.is_downloaded alone: replace the current
implementation of isDownloaded (and its callers if needed) so it returns
adapterLocalPath(entry) != null (or adapterLocalPath(entry) != null &&
entry.is_downloaded == true if you want both flags), ensuring apply() always has
a non-null path; alternatively, if you must keep the is_downloaded flag
semantics, ensure entry.local_path is populated whenever entry.is_downloaded ==
true before using isDownloaded. Use the existing adapterLocalPath and apply()
symbols to locate the logic to update.

In `@examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/STTViewModel.swift`:
- Around line 264-296: performHybridTranscription is running on the main actor
but calls the synchronous HybridSTTRouter.transcribe(_:options:) which can block
the UI; to fix, run the transcribe call off the main actor by wrapping it in a
Task.detached (e.g., Task.detached(priority: .userInitiated) { try
router.transcribe(audioBuffer, options: options) }.value) and await the result,
then assign transcription and hybridRouting back on the main actor; keep the
surrounding calls to registerCloudProvider() and
ensureHybridRouter(offlineModelId:onlineModelId:) as-is but ensure only the
synchronous router.transcribe call is moved into the detached task.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/errors/SDKException.kt`:
- Around line 87-94: The telemetryProperties getter in SDKException currently
includes raw error.message which can leak sensitive/free-form text; change
telemetryProperties (in class SDKException) to stop shipping error.message —
either remove the "error_message" entry entirely or replace it with a
deterministic/sanitized placeholder (e.g., "redacted" or a fixed token) or a
hashed/safe code; keep "error_code" and "error_category" as-is and ensure any
construction paths (make(...)/from(...)) do not reintroduce raw messages into
telemetryProperties.

In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt`:
- Around line 85-93: storeSDKParams currently writes keys with a short-circuited
&& chain so a mid-way failure can leave a mixed state; change storeSDKParams to
perform an atomic write: use the underlying secure storage's transaction/editor
API (or equivalent batch write) to set KEY_API_KEY, KEY_BASE_URL and
KEY_ENVIRONMENT in a single commit, and return success only if the commit
succeeds; if no editor/transaction is available, instead write the three values
sequentially but on any failure immediately clear/remove any keys already
written (KEY_API_KEY, KEY_BASE_URL, KEY_ENVIRONMENT) before returning false so
retrieveSDKParams cannot read a partial tuple.

In `@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/Cloud.kt`:
- Around line 207-209: The current code throws a generic SDKException.operation
when RunAnywhereBridge.racCloudRegisterSttProvider returns a non-success rc,
losing the original rac_result_t and c_abi_code; change the error path to map
the native rc into the proper SDKException using the existing C-ABI mapping
helper instead of manufacturing a new operation error—e.g., replace the throw
with a call to the throwIfCAbiErrorAsException() extension (or the project’s
equivalent mapping function) on the returned rc so the resulting SDKException
wraps an SDKError containing code, category, message and the original
c_abi_code; keep the call to RunAnywhereBridge.racCloudRegisterSttProvider(name,
native) and only modify the error handling around rc to use the mapped
exception.

In `@sdk/shared/proto-ts/src/convenience/rag_convenience.ts`:
- Around line 50-55: Add a client-side check to ensure chunk_overlap is strictly
less than chunk_size: in the validation block that currently checks
m.chunkOverlap (and throws ValidationError with fieldPath
'RAGConfiguration.chunk_overlap'), also verify that if m.chunkSize is defined
then m.chunkOverlap < m.chunkSize (reject if m.chunkOverlap >= m.chunkSize) and
throw a ValidationError with a clear message including both values; keep the
existing non-negative check and reference the same symbols (m.chunkOverlap,
m.chunkSize, ValidationError, RAGConfiguration.chunk_overlap) so invalid configs
are caught before reaching the native boundary.

---

Outside diff comments:
In
`@examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt`:
- Around line 243-274: generateWithTools currently updates message text/tool
info but does not populate GenerationStats, so tool-assisted messages lack
token/latency/throughput analytics; extract the relevant stats from result
(e.g., tokens_generated, tokens_input, generation_time_ms or similarly named
fields returned by RunAnywhere.generateWithTools), compute throughput if needed,
build a GenerationStats instance, and include it in the message update
(messages[index].copy(..., generationStats = yourStats)) analogous to how
generateReply/streamReply set stats so ChatDetailsSheet can display token counts
and latency.

---

Nitpick comments:
In
`@examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/SpeechToTextView.swift`:
- Around line 334-361: Add a UI control to hybridConfigurationSection so users
can adjust viewModel.hybridMinBattery: insert a small VStack (like the existing
fallback threshold block for hybridConfidenceThreshold) that displays "Min
battery \(Int(viewModel.hybridMinBattery))%" and binds a Slider to
$viewModel.hybridMinBattery with range 0...100 and an appropriate step (e.g.,
5), matching the styling (font .caption, foregroundColor .secondary) and
placement within the hybridConfigurationSection so the hybridMinBattery setting
is editable alongside hybridConfidenceThreshold.
🪄 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: fad2b98b-3309-487c-9626-1e99ae23c445

📥 Commits

Reviewing files that changed from the base of the PR and between 009017d and f5e5460.

⛔ Files ignored due to path filters (8)
  • sdk/runanywhere-commons/src/generated/proto/rag.pb.cc is excluded by !**/generated/**
  • sdk/runanywhere-commons/src/generated/proto/rag.pb.h is excluded by !**/generated/**
  • sdk/runanywhere-flutter/packages/runanywhere/lib/generated/convenience/ra_convenience.dart is excluded by !**/generated/**
  • sdk/runanywhere-flutter/packages/runanywhere/lib/generated/rag.pb.dart is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/RAGConfiguration.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/convenience/RAConvenience.kt is excluded by !**/generated/**
  • sdk/runanywhere-swift/Sources/RunAnywhere/Generated/RAConvenience.swift is excluded by !**/generated/**
  • sdk/runanywhere-swift/Sources/RunAnywhere/Generated/rag.pb.swift is excluded by !**/generated/**
📒 Files selected for processing (35)
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/lora/LoraViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tts/TtsViewModel.kt
  • examples/android/RunAnywhereAI/scripts/sync-solutions-yamls.sh
  • examples/ios/RunAnywhereAI/RunAnywhereAI/App/RunAnywhereAIApp.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/STTViewModel.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/SpeechToTextView.swift
  • idl/codegen/generate_swift_convenience.py
  • idl/rag.proto
  • sdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cpp
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeSDKEvents.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeStructuredOutput.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeTelemetry.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/errors/SDKException.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/Cloud.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/HybridRoutingPolicy.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/RunAnywhere.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/events/EventBus.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/ToolCallingTypes.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Models/ModelTypesArtifacts.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Models/RunAnywhereModelLifecycle.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/DownloadStageExtensions.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VoiceAgent/VoiceAgentTypes.kt
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/CppBridge.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Telemetry.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/AppleDeviceStateProvider.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/RAG/RAGProto+Helpers.swift
  • sdk/shared/proto-ts/src/convenience/rag_convenience.ts
  • sdk/shared/proto-ts/src/rag.ts
💤 Files with no reviewable changes (1)
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeStructuredOutput.kt
✅ Files skipped from review due to trivial changes (3)
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/HybridRoutingPolicy.kt
  • sdk/shared/proto-ts/src/rag.ts
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp

Comment on lines 126 to +131
fun isDownloaded(entry: LoraAdapterCatalogEntry): Boolean =
downloadedPaths.containsKey(entry.id) || findAdapterFile(entry) != null

private fun adapterFilename(entry: LoraAdapterCatalogEntry): String =
entry.filename.ifBlank { entry.url.substringAfterLast('/').substringBefore('?') }
adapterLocalPath(entry) != null || entry.is_downloaded == true

private fun findAdapterFile(entry: LoraAdapterCatalogEntry): File? {
val name = adapterFilename(entry).ifBlank { return null }
return getApplication<Application>().filesDir
.walkTopDown()
.firstOrNull { it.isFile && it.name == name && it.length() > 0 }
}
private fun adapterLocalPath(entry: LoraAdapterCatalogEntry): String? =
downloadedPaths[entry.id]
?: entry.local_path?.takeIf { it.isNotBlank() }

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

isDownloaded can return true when apply will fail.

isDownloaded returns true when entry.is_downloaded == true even if adapterLocalPath(entry) is null. However, apply() at Line 72 requires a non-null path and shows "Adapter not downloaded yet" otherwise. This creates a confusing UX where an adapter appears downloaded but cannot be applied.

Consider aligning the conditions:

Proposed fix
 fun isDownloaded(entry: LoraAdapterCatalogEntry): Boolean =
-    adapterLocalPath(entry) != null || entry.is_downloaded == true
+    adapterLocalPath(entry) != null

Or ensure entry.local_path is populated whenever entry.is_downloaded is true.

📝 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
fun isDownloaded(entry: LoraAdapterCatalogEntry): Boolean =
downloadedPaths.containsKey(entry.id) || findAdapterFile(entry) != null
private fun adapterFilename(entry: LoraAdapterCatalogEntry): String =
entry.filename.ifBlank { entry.url.substringAfterLast('/').substringBefore('?') }
adapterLocalPath(entry) != null || entry.is_downloaded == true
private fun findAdapterFile(entry: LoraAdapterCatalogEntry): File? {
val name = adapterFilename(entry).ifBlank { return null }
return getApplication<Application>().filesDir
.walkTopDown()
.firstOrNull { it.isFile && it.name == name && it.length() > 0 }
}
private fun adapterLocalPath(entry: LoraAdapterCatalogEntry): String? =
downloadedPaths[entry.id]
?: entry.local_path?.takeIf { it.isNotBlank() }
fun isDownloaded(entry: LoraAdapterCatalogEntry): Boolean =
adapterLocalPath(entry) != null
private fun adapterLocalPath(entry: LoraAdapterCatalogEntry): String? =
downloadedPaths[entry.id]
?: entry.local_path?.takeIf { it.isNotBlank() }
🤖 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/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/lora/LoraViewModel.kt`
around lines 126 - 131, Change isDownloaded to reflect the actual usable local
path instead of relying on entry.is_downloaded alone: replace the current
implementation of isDownloaded (and its callers if needed) so it returns
adapterLocalPath(entry) != null (or adapterLocalPath(entry) != null &&
entry.is_downloaded == true if you want both flags), ensuring apply() always has
a non-null path; alternatively, if you must keep the is_downloaded flag
semantics, ensure entry.local_path is populated whenever entry.is_downloaded ==
true before using isDownloaded. Use the existing adapterLocalPath and apply()
symbols to locate the logic to update.

Comment on lines +264 to +296
private func performHybridTranscription() async {
guard !audioBuffer.isEmpty else {
errorMessage = "No audio recorded"
return
}
guard let offlineModelId = selectedModelId else {
errorMessage = "No STT model loaded"
return
}

logger.info("Starting hybrid transcription of \(self.audioBuffer.count) bytes")
isTranscribing = true
transcription = ""
hybridRouting = nil

do {
let onlineModelId = try registerCloudProvider()
let router = try ensureHybridRouter(offlineModelId: offlineModelId, onlineModelId: onlineModelId)
var options = HybridTranscribeOptions()
options.sampleRate = 16_000
options.audioFormat = CloudAudioFormat.wav.nativeValue

let result = try router.transcribe(audioBuffer, options: options)
transcription = result.text
hybridRouting = result.routing
logger.info("Hybrid transcription complete: \(result.text)")
} catch {
logger.error("Hybrid transcription failed: \(error.localizedDescription)")
errorMessage = "Hybrid transcription failed: \(error.localizedDescription)"
}

isTranscribing = false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if HybridSTTRouter.transcribe is async or synchronous
ast-grep --pattern 'func transcribe($$$) $_'
rg -n "func transcribe" --type swift -A 5 | head -60

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 6484


🏁 Script executed:

#!/bin/bash
# Inspect HybridSTTRouter.transcribe implementation details
sed -n '200,320p' sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift

# Inspect underlying STT transcribe proto ABI invocation used by the router (if referenced)
rg -n "HybridSTTRouter\\.transcribe|transcribe\\(" sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift
rg -n "STTGeneratedProtoABI\\.transcribe|symbol: STTGeneratedProtoABI\\.transcribe|invoke\\(" sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift sdk/runanywhere-swift/Sources/RunAnywhere/Generated/ModalityProtoABI+Generated.swift
sed -n '340,430p' sdk/runanywhere-swift/Sources/RunAnywhere/Generated/ModalityProtoABI+Generated.swift

# Inspect the CppBridge/STT transcribe path if it is used by the generated ABI
rg -n "transcribe\\(" sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge\\+STT.swift
sed -n '150,270p' sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+STT.swift

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 17345


🏁 Script executed:

#!/bin/bash
# Find HybridSTTRouter type definition and whether it is Sendable
rg -n "struct HybridSTTRouter|class HybridSTTRouter" sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift
sed -n '1,120p' sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift
rg -n "Sendable" sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 5621


Move HybridSTTRouter.transcribe off the @MainActor to avoid UI stalls
In examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/STTViewModel.swift (lines 264-296), performHybridTranscription() runs on the main actor, but HybridSTTRouter.transcribe(_:options:) is a synchronous throws -> HybridTranscribeResult call that directly invokes rac_stt_hybrid_router_transcribe_proto, so it can block the UI thread during native transcription.

let result = try await Task.detached(priority: .userInitiated) {
    try router.transcribe(audioBuffer, options: options)
}.value
🤖 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/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/STTViewModel.swift`
around lines 264 - 296, performHybridTranscription is running on the main actor
but calls the synchronous HybridSTTRouter.transcribe(_:options:) which can block
the UI; to fix, run the transcribe call off the main actor by wrapping it in a
Task.detached (e.g., Task.detached(priority: .userInitiated) { try
router.transcribe(audioBuffer, options: options) }.value) and await the result,
then assign transcription and hybridRouting back on the main actor; keep the
surrounding calls to registerCloudProvider() and
ensureHybridRouter(offlineModelId:onlineModelId:) as-is but ensure only the
synchronous router.transcribe call is moved into the detached task.

Comment on lines +87 to +94
/** Telemetry-only properties (lightweight, safe to ship). */
val telemetryProperties: Map<String, String>
get() =
mapOf(
"error_code" to code.name,
"error_category" to category.name,
"error_message" to error.message,
)

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 | 🟠 Major | ⚡ Quick win

Don't ship raw exception text in telemetryProperties.

error.message is free-form here (make(...)/from(...) can wrap arbitrary throwable text), so this “safe to ship” map can carry request URLs, backend payloads, or token fragments into telemetry. Keep the structured code/category fields, but drop or explicitly redact the message field before exposing it for telemetry.

Suggested fix
     val telemetryProperties: Map<String, String>
         get() =
             mapOf(
                 "error_code" to code.name,
                 "error_category" to category.name,
-                "error_message" to error.message,
             )
📝 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
/** Telemetry-only properties (lightweight, safe to ship). */
val telemetryProperties: Map<String, String>
get() =
mapOf(
"error_code" to code.name,
"error_category" to category.name,
"error_message" to error.message,
)
/** Telemetry-only properties (lightweight, safe to ship). */
val telemetryProperties: Map<String, String>
get() =
mapOf(
"error_code" to code.name,
"error_category" to category.name,
)
🤖 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-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/errors/SDKException.kt`
around lines 87 - 94, The telemetryProperties getter in SDKException currently
includes raw error.message which can leak sensitive/free-form text; change
telemetryProperties (in class SDKException) to stop shipping error.message —
either remove the "error_message" entry entirely or replace it with a
deterministic/sanitized placeholder (e.g., "redacted" or a fixed token) or a
hashed/safe code; keep "error_code" and "error_category" as-is and ensure any
construction paths (make(...)/from(...)) do not reintroduce raw messages into
telemetryProperties.

Comment on lines +85 to +93
fun storeSDKParams(params: SDKInitParams): Boolean {
val success =
setString(KEY_API_KEY, params.apiKey) &&
setString(KEY_BASE_URL, params.baseURL) &&
setString(KEY_ENVIRONMENT, params.environment.wireString)
if (success) {
logger.info("SDK parameters stored securely")
}
return success

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 | 🟠 Major | ⚡ Quick win

Persist the SDK init tuple atomically.

This short-circuited && chain can leave a mixed {apiKey, baseURL, environment} behind when one write fails after an earlier key succeeded. retrieveSDKParams() will then reconstruct that hybrid config on the next launch and can point a new API key at an old backend. Write all three keys in one editor transaction, or clear previously written keys on failure.

🤖 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-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt`
around lines 85 - 93, storeSDKParams currently writes keys with a
short-circuited && chain so a mid-way failure can leave a mixed state; change
storeSDKParams to perform an atomic write: use the underlying secure storage's
transaction/editor API (or equivalent batch write) to set KEY_API_KEY,
KEY_BASE_URL and KEY_ENVIRONMENT in a single commit, and return success only if
the commit succeeds; if no editor/transaction is available, instead write the
three values sequentially but on any failure immediately clear/remove any keys
already written (KEY_API_KEY, KEY_BASE_URL, KEY_ENVIRONMENT) before returning
false so retrieveSDKParams cannot read a partial tuple.

Comment on lines 207 to +209
val rc = RunAnywhereBridge.racCloudRegisterSttProvider(name, native)
check(rc == RunAnywhereBridge.RAC_SUCCESS) {
"Failed to register cloud provider '$name' (rc=$rc)"
if (rc != RunAnywhereBridge.RAC_SUCCESS) {
throw SDKException.operation("Failed to register cloud provider '$name' (rc=$rc)")

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 | 🟠 Major | ⚡ Quick win

Map the native rc instead of collapsing it to ERROR_CODE_GENERATION_FAILED.

SDKException.operation(...) hides the original rac_result_t and reclassifies provider-registration failures as a generic generation error, so callers lose the exact JNI failure reason and c_abi_code. Throw the mapped RAC exception here instead of manufacturing a new one.

As per coding guidelines, "Error handling must use SDKException wrapping a proto SDKError(code, category, message, c_abi_code). Factory methods must map C ABI negative return codes to typed exceptions via throwIfCAbiErrorAsException() extension."

🤖 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-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/Cloud.kt`
around lines 207 - 209, The current code throws a generic SDKException.operation
when RunAnywhereBridge.racCloudRegisterSttProvider returns a non-success rc,
losing the original rac_result_t and c_abi_code; change the error path to map
the native rc into the proper SDKException using the existing C-ABI mapping
helper instead of manufacturing a new operation error—e.g., replace the throw
with a call to the throwIfCAbiErrorAsException() extension (or the project’s
equivalent mapping function) on the returned rc so the resulting SDKException
wraps an SDKError containing code, category, message and the original
c_abi_code; keep the call to RunAnywhereBridge.racCloudRegisterSttProvider(name,
native) and only modify the error handling around rc to use the mapped
exception.

Source: Coding guidelines

Comment on lines +50 to +55
if (m.chunkOverlap !== undefined && (m.chunkOverlap < 0)) {
throw new ValidationError({
fieldPath: 'RAGConfiguration.chunk_overlap',
message: `chunk_overlap must be in >= 0 (got ${m.chunkOverlap})`,
});
}

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 | 🟠 Major | ⚡ Quick win

Add validation for chunk_overlap < chunk_size.

The C++ native validation enforces chunk_overlap < chunk_size (see sdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cpp lines 246-250), but the TypeScript validator only checks chunkOverlap >= 0. This allows invalid configurations where overlap equals or exceeds chunk size to pass client-side validation and fail later at the native boundary.

🔍 Proposed fix
   if (m.chunkOverlap !== undefined && (m.chunkOverlap < 0)) {
     throw new ValidationError({
       fieldPath: 'RAGConfiguration.chunk_overlap',
       message: `chunk_overlap must be in >= 0 (got ${m.chunkOverlap})`,
     });
   }
+  if (m.chunkSize !== undefined && m.chunkOverlap !== undefined && m.chunkOverlap >= m.chunkSize) {
+    throw new ValidationError({
+      fieldPath: 'RAGConfiguration.chunk_overlap',
+      message: `chunk_overlap must be < chunk_size (got overlap=${m.chunkOverlap}, size=${m.chunkSize})`,
+    });
+  }
 };
🤖 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/shared/proto-ts/src/convenience/rag_convenience.ts` around lines 50 - 55,
Add a client-side check to ensure chunk_overlap is strictly less than
chunk_size: in the validation block that currently checks m.chunkOverlap (and
throws ValidationError with fieldPath 'RAGConfiguration.chunk_overlap'), also
verify that if m.chunkSize is defined then m.chunkOverlap < m.chunkSize (reject
if m.chunkOverlap >= m.chunkSize) and throw a ValidationError with a clear
message including both values; keep the existing non-negative check and
reference the same symbols (m.chunkOverlap, m.chunkSize, ValidationError,
RAGConfiguration.chunk_overlap) so invalid configs are caught before reaching
the native boundary.

React Native:
- Fix httpSetupApplicable end-to-end: parse proto field 11 (http_applicable)
  in InitBridge, return serialized RASdkInitResult from the phase-2/retry
  nitro methods, and mirror Swift's 3-branch ensureServicesReady so offline
  init stops retrying HTTP on every call
- Clean renames to Swift names: downloadModel/downloadModelStream swap,
  CloudSTT.registerModel, HybridSTTRouter (example app updated)
- Wire embeddings namespace, refreshModelRegistry, AudioConvert + pcm16ToWav
  onto the facade; add registerArchiveModel, LoRA registerArtifact/download
  chain, custom cloud-STT providers (new nitro trampoline), RAG quartet
- Guard now propagates errors with Swift's coverage table (try?-equivalents
  only at listModels/getModel/refreshModelRegistry/loadModel)
- EventBus typed payload streams + modelLifecycle; logging redaction /
  __DEV__ gating / Sentry destination fixes; SDKException recoverySuggestion
  + rc-result bridge over new resultToProtoErrorProto; generic
  HandleStreamAdapter with deterministic isFinal teardown

Flutter:
- speak() now synthesizes AND plays via SDK-owned AudioPlaybackManager;
  AudioCaptureManager moves mic capture into the SDK (example app audio
  services deleted per layering rule)
- Model-paths base dir set in Phase 1 (fixes registerModel race); add
  aggregateStream, public SDK-events quintet, refreshModelRegistry params,
  validateCalls, LoRA registerArtifact/download, hybrid cancel(), EventBus
  alignment (sdkEvents/stt/tts/error + typed payloads + modelLifecycle)
- Breaking trim to Swift parity: remove flat aliases and VAD/tools/storage/
  downloads/VLM/voice extras Swift lacks; rename transcribeStreamSession ->
  transcribeStream; privatize ensureServicesReady; drop runDiscoveryIfNeeded
- Scrub stale AGENTS.md claims (hardware namespace, internal/, data/network/)

Gates: swift build, yarn typecheck (core+example), jest 25/25, nitrogen
regen, RunAnywhereCore pod build, flutter analyze (SDK+example), example
debug APK build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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 (1)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_hybrid_stt.dart (1)

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

Docstring appears misplaced after method insertion.

The docstring on lines 391-393 starts with "Destroy a router handle…" but is attached to cancelRouter. This looks like cancelRouter was inserted mid-docstring. The destroyRouter method at line 403 is now undocumented.

📝 Suggested fix
-  /// Destroy a router handle. The wrapped services are NOT freed here — callers
-  /// clear the slots + [destroyService] them first (see header UAF note).
   /// Cancel an in-flight transcribe, if any. Best-effort: commons treats
   /// this as a no-op until an STT engine exposes a cancel op (see
   /// rac_stt_hybrid_router_cancel). Mirrors Swift HybridSTTRouter.cancel().
   void cancelRouter(RacHandle handle) {
     final cancelFn = _routerCancel;
     if (cancelFn == null) {
       _logger.debug('rac_stt_hybrid_router_cancel unavailable');
       return;
     }
     cancelFn(handle);
   }

+  /// Destroy a router handle. The wrapped services are NOT freed here — callers
+  /// clear the slots + [destroyService] them first (see header UAF note).
   void destroyRouter(RacHandle handle) {
🤖 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_hybrid_stt.dart`
around lines 391 - 408, The docstring describing "Destroy a router handle…" was
accidentally attached to cancelRouter; move that docstring from above
cancelRouter to directly above destroyRouter and ensure destroyRouter has the
correct doc comment, while leaving cancelRouter with a brief comment noting it
is a best-effort no-op wrapper around the native _routerCancel; confirm
references to RacHandle, nullptr, _routerDestroy and _routerCancel remain
correct and keep the _logger.debug message in cancelRouter intact.
🧹 Nitpick comments (8)
sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.ts (1)

785-794: ⚡ Quick win

Variable shadowing: model parameter overwritten with registry fetch.

The function accepts model: ModelInfo as a parameter (line 712), but then re-fetches and overwrites it on line 793:

const model = ModelInfoCodec.decode(modelBytes);
modelForImport = model;

This shadows the incoming parameter. If re-fetching from the registry is intentional (to get fresh state), consider renaming either the parameter or the local variable to avoid confusion. For example:

  • Rename parameter to inputModel
  • Rename fetched model to registryModel
🤖 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-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere`+ModelRegistry.ts
around lines 785 - 794, The incoming parameter named model is being shadowed by
the local decoded value from ModelInfoCodec.decode(modelBytes); avoid this by
renaming one of them (e.g., rename the function parameter model to inputModel,
or rename the decoded local to registryModel) and update all usages accordingly
(notably modelForImport assignment and any subsequent references) so the intent
is clear whether you use the caller-provided ModelInfo or the registry-fetched
ModelInfo.
examples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxproj (2)

605-617: 💤 Low value

Duplicate -DRCT_REMOVE_LEGACY_ARCH=1 flag in C++ compiler flags.

OTHER_CPLUSPLUSFLAGS includes the flag twice:

  1. Inherited via $(OTHER_CFLAGS) (line 610)
  2. Explicitly on line 616

While Xcode will deduplicate this, the explicit flag on line 616 is redundant since line 610 already brings it in via $(OTHER_CFLAGS). Consider removing the explicit entry on line 616 for clarity.

♻️ Proposed cleanup
 				OTHER_CPLUSPLUSFLAGS = (
 					"$(OTHER_CFLAGS)",
 					"-DFOLLY_NO_CONFIG",
 					"-DFOLLY_MOBILE=1",
 					"-DFOLLY_USE_LIBCPP=1",
 					"-DFOLLY_CFG_NO_COROUTINES=1",
 					"-DFOLLY_HAVE_CLOCK_GETTIME=1",
-					"-DRCT_REMOVE_LEGACY_ARCH=1",
 				);
🤖 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/ios/RunAnywhereAI.xcodeproj/project.pbxproj`
around lines 605 - 617, OTHER_CPLUSPLUSFLAGS currently expands "$(OTHER_CFLAGS)"
which already contains "-DRCT_REMOVE_LEGACY_ARCH=1", and the same flag is
redundantly listed again explicitly; remove the explicit
"-DRCT_REMOVE_LEGACY_ARCH=1" entry from OTHER_CPLUSPLUSFLAGS so it only inherits
that define via "$(OTHER_CFLAGS)", leaving the other defines (like -DFOLLY_*)
intact.

687-699: 💤 Low value

Duplicate -DRCT_REMOVE_LEGACY_ARCH=1 flag in C++ compiler flags (Release).

Same redundancy as Debug configuration: OTHER_CPLUSPLUSFLAGS includes the flag twice (via $(OTHER_CFLAGS) on line 692 and explicitly on line 698).

♻️ Proposed cleanup
 				OTHER_CPLUSPLUSFLAGS = (
 					"$(OTHER_CFLAGS)",
 					"-DFOLLY_NO_CONFIG",
 					"-DFOLLY_MOBILE=1",
 					"-DFOLLY_USE_LIBCPP=1",
 					"-DFOLLY_CFG_NO_COROUTINES=1",
 					"-DFOLLY_HAVE_CLOCK_GETTIME=1",
-					"-DRCT_REMOVE_LEGACY_ARCH=1",
 				);
🤖 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/ios/RunAnywhereAI.xcodeproj/project.pbxproj`
around lines 687 - 699, The Release build currently injects
-DRCT_REMOVE_LEGACY_ARCH=1 twice because OTHER_CPLUSPLUSFLAGS expands
"$(OTHER_CFLAGS)" which already contains "-DRCT_REMOVE_LEGACY_ARCH=1" and then
also lists it explicitly; remove the duplicate by deleting the explicit
"-DRCT_REMOVE_LEGACY_ARCH=1" entry from OTHER_CPLUSPLUSFLAGS (or alternatively
remove it from OTHER_CFLAGS) so the flag appears only once, locating the
settings under the Release configuration where OTHER_CFLAGS and
OTHER_CPLUSPLUSFLAGS are defined.
sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/CloudSTT.ts (1)

306-312: 💤 Low value

Inconsistent input validation between registerProvider and unregisterProvider.

registerProvider throws SDKException.invalidInput for an empty name (line 254-256), but unregisterProvider silently returns. This inconsistency could mask bugs where callers pass empty strings.

♻️ Optional: align validation with registerProvider
   async unregisterProvider(name: string): Promise<void> {
-    if (!name || !isNativeModuleAvailable()) {
+    if (!name) {
+      return; // or throw SDKException.invalidInput(...) to match registerProvider
+    }
+    if (!isNativeModuleAvailable()) {
       return;
     }
     registeredProviders.delete(name);
     await requireNativeModule().cloudUnregisterSttProvider(name);
   },
🤖 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-react-native/packages/core/src/Public/Extensions/Hybrid/CloudSTT.ts`
around lines 306 - 312, unregisterProvider currently silently returns when name
is empty whereas registerProvider throws SDKException.invalidInput; change
unregisterProvider to perform the same input validation as registerProvider by
throwing SDKException.invalidInput for empty/invalid name, keep the
isNativeModuleAvailable() guard, and then proceed to delete from
registeredProviders and call
requireNativeModule().cloudUnregisterSttProvider(name); ensure you reference
SDKException.invalidInput, unregisterProvider, registerProvider,
isNativeModuleAvailable, registeredProviders.delete, and
requireNativeModule().cloudUnregisterSttProvider in the fix.
sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_lora.dart (1)

264-270: 💤 Low value

Consider using a download-specific exception type.

SDKException.invalidConfiguration is semantically incorrect for download failures. A download can fail due to network issues, insufficient storage, or server errors—none of which are "invalid configuration." Consider using or adding a download-specific factory (e.g., SDKException.downloadFailed).

🤖 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/public/capabilities/runanywhere_lora.dart`
around lines 264 - 270, The code throws SDKException.invalidConfiguration when
progress.state == DownloadState.DOWNLOAD_STATE_FAILED which mischaracterizes
download failures; replace this with a download-specific exception by adding (or
using) a factory such as SDKException.downloadFailed and call it with the
download error message (use progress.errorMessage if present, otherwise include
a contextual message like 'LoRA adapter download failed for ${entry.id}');
update the throw site that currently references
SDKException.invalidConfiguration and ensure the new factory constructs an
appropriate exception type for network/storage/server download errors.
sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp (1)

351-361: 💤 Low value

Memory may not be freed on all code paths, or freed when uninitialized.

The rac_proto_buffer_t out{} is zero-initialized, so out.data and out.error_message start as nullptr. However:

  1. If fn(rc, &out) fails, out.error_message might contain an error string that should be freed
  2. The current code always frees both regardless of status, which is fine for null but may miss the error message content

The code looks safe due to zero-init, but consider logging the error message before freeing it on failure:

     if (status == RAC_SUCCESS && out.data && out.size > 0) {
       buffer = ArrayBuffer::copy(out.data, out.size);
     } else {
+      if (out.error_message) {
+        LOGW("resultToProtoErrorProto failed: %s", out.error_message);
+      }
       buffer = ArrayBuffer::allocate(0);
     }
🤖 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-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp`
around lines 351 - 361, The code frees out.data and out.error_message after
calling fn(rc, &out) but never surfaces the error message when status !=
RAC_SUCCESS; update the block around rac_proto_buffer_t out, rac_result_t status
and fn(rc, &out) so that when status indicates failure you log the contents of
out.error_message (if non-null) before freeing it, then free both out.data and
out.error_message (or guard frees with null checks); keep the successful path
creating ArrayBuffer::copy(out.data, out.size) and still free allocated buffers
afterwards.
sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tools.dart (1)

46-48: 💤 Low value

Unused typedef after registerTypedTool removal.

TypedToolExecutor is no longer used in this file since registerTypedTool was removed. Consider removing it if no external callers depend on it, or add a comment indicating it's kept for external use.

🤖 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/public/capabilities/runanywhere_tools.dart`
around lines 46 - 48, The typedef TypedToolExecutor is now unused in this file
after removal of registerTypedTool; remove the TypedToolExecutor declaration (or
if it must be preserved for external API compatibility, add a clear comment
above TypedToolExecutor stating it is intentionally retained for external
callers) and update any exports/docs accordingly; look for the TypedToolExecutor
symbol in this file to either delete the typedef or add the explanatory comment
and ensure no references remain to registerTypedTool.
sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.ts (1)

550-556: 💤 Low value

Consider a more accurate error category.

The error is thrown when download succeeded but localPath was not persisted. This is a storage/registry issue, not a network issue. ERROR_CATEGORY_STORAGE or ERROR_CATEGORY_INTERNAL would be more precise than ERROR_CATEGORY_NETWORK.

However, if this mirrors Swift for cross-platform parity, this may be intentional.

🤖 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-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere`+LoRA.ts
around lines 550 - 556, The thrown SDKException in RunAnywhere+LoRA.ts when
localPath is missing uses ErrorCategory.ERROR_CATEGORY_NETWORK but this is a
storage/registry persistence problem; update the error category in the thrown
exception (in the block that checks localPath for the LoRA adapter identified by
entry.id) to a more accurate value such as ErrorCategory.ERROR_CATEGORY_STORAGE
or ErrorCategory.ERROR_CATEGORY_INTERNAL, and if cross-platform parity with the
Swift implementation is required, match the Swift choice instead.
🤖 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
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_lora.dart`:
- Around line 166-168: The check using entry.url.split('/').isNotEmpty is
ineffective because split() always returns at least one element; instead check
entry.url.isNotEmpty (or entry.url.trim().isNotEmpty) before splitting and use
the last path segment only when the url is non-empty; assign urlTail =
entry.url.isNotEmpty ? entry.url.split('/').last : entry.filename (or a safe
fallback) and ensure artifactFilename logic uses entry.filename when both url
tail and entry.filename would otherwise be empty—update the code around the
urlTail/artifactFilename assignments (referencing variables urlTail,
artifactFilename, entry.url, entry.filename) accordingly.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tts.dart`:
- Around line 218-220: In speak(), don't silently skip playback when wavData is
null/empty: add an explicit guard after generating wavData that checks if
wavData == null || wavData.isEmpty and immediately return a failing
TTSSpeakResult (success = false) with an explanatory error message instead of
proceeding to await _playback.play; keep success paths unchanged and reference
the speak() method, the wavData variable, and the _playback.play(...) call to
locate where to insert the failure return.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/audio/audio_convert.dart`:
- Around line 63-69: The pcm16ToWav function should validate the sampleRate
input before building WAV metadata: check that sampleRate is a positive integer
(> 0) at the top of pcm16ToWav and throw a clear ArgumentError (or similar) if
it's invalid; update function pcm16ToWav to guard against sampleRate <= 0 and
return early by raising the error so invalid headers are never produced.

In
`@sdk/runanywhere-react-native/packages/core/src/Adapters/HandleStreamAdapter.ts`:
- Around line 204-224: The new HandleFanOut created in subscribe is left in
registry.cache when fan.attach(sub) fails, causing future callers to get a
broken fan; update subscribe so that after creating and setting fan in
registry.cache, if fan.attach(sub) returns null you call
registry.cache.delete(handle) (or otherwise remove the newly inserted
HandleFanOut) before invoking onError/onDone and returning, ensuring no orphaned
cache entry remains; reference the subscribe function, registry.cache,
HandleFanOut constructor, fan.attach, and the cancel/null check to locate where
to remove the cache entry.

In
`@sdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Services/LoggingManager.ts`:
- Around line 244-252: The sanitizeMetadata implementation currently skips
arrays, so nested objects inside arrays (e.g., users: [{ token: 'secret' }])
aren't redacted; update the loop in sanitizeMetadata to detect
Array.isArray(value) and, when true, produce sanitized[key] = value.map(elem =>
(elem !== null && typeof elem === 'object' && !Array.isArray(elem)) ?
sanitizeMetadata(elem as Record<string, unknown>) ?? {} : elem) (and also handle
nested arrays recursively if needed), while preserving the existing
shouldRedact(key) check and behavior for non-object values.

---

Outside diff comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_hybrid_stt.dart`:
- Around line 391-408: The docstring describing "Destroy a router handle…" was
accidentally attached to cancelRouter; move that docstring from above
cancelRouter to directly above destroyRouter and ensure destroyRouter has the
correct doc comment, while leaving cancelRouter with a brief comment noting it
is a best-effort no-op wrapper around the native _routerCancel; confirm
references to RacHandle, nullptr, _routerDestroy and _routerCancel remain
correct and keep the _logger.debug message in cancelRouter intact.

---

Nitpick comments:
In
`@examples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxproj`:
- Around line 605-617: OTHER_CPLUSPLUSFLAGS currently expands "$(OTHER_CFLAGS)"
which already contains "-DRCT_REMOVE_LEGACY_ARCH=1", and the same flag is
redundantly listed again explicitly; remove the explicit
"-DRCT_REMOVE_LEGACY_ARCH=1" entry from OTHER_CPLUSPLUSFLAGS so it only inherits
that define via "$(OTHER_CFLAGS)", leaving the other defines (like -DFOLLY_*)
intact.
- Around line 687-699: The Release build currently injects
-DRCT_REMOVE_LEGACY_ARCH=1 twice because OTHER_CPLUSPLUSFLAGS expands
"$(OTHER_CFLAGS)" which already contains "-DRCT_REMOVE_LEGACY_ARCH=1" and then
also lists it explicitly; remove the duplicate by deleting the explicit
"-DRCT_REMOVE_LEGACY_ARCH=1" entry from OTHER_CPLUSPLUSFLAGS (or alternatively
remove it from OTHER_CFLAGS) so the flag appears only once, locating the
settings under the Release configuration where OTHER_CFLAGS and
OTHER_CPLUSPLUSFLAGS are defined.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_lora.dart`:
- Around line 264-270: The code throws SDKException.invalidConfiguration when
progress.state == DownloadState.DOWNLOAD_STATE_FAILED which mischaracterizes
download failures; replace this with a download-specific exception by adding (or
using) a factory such as SDKException.downloadFailed and call it with the
download error message (use progress.errorMessage if present, otherwise include
a contextual message like 'LoRA adapter download failed for ${entry.id}');
update the throw site that currently references
SDKException.invalidConfiguration and ensure the new factory constructs an
appropriate exception type for network/storage/server download errors.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tools.dart`:
- Around line 46-48: The typedef TypedToolExecutor is now unused in this file
after removal of registerTypedTool; remove the TypedToolExecutor declaration (or
if it must be preserved for external API compatibility, add a clear comment
above TypedToolExecutor stating it is intentionally retained for external
callers) and update any exports/docs accordingly; look for the TypedToolExecutor
symbol in this file to either delete the typedef or add the explanatory comment
and ensure no references remain to registerTypedTool.

In `@sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp`:
- Around line 351-361: The code frees out.data and out.error_message after
calling fn(rc, &out) but never surfaces the error message when status !=
RAC_SUCCESS; update the block around rac_proto_buffer_t out, rac_result_t status
and fn(rc, &out) so that when status indicates failure you log the contents of
out.error_message (if non-null) before freeing it, then free both out.data and
out.error_message (or guard frees with null checks); keep the successful path
creating ArrayBuffer::copy(out.data, out.size) and still free allocated buffers
afterwards.

In
`@sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/CloudSTT.ts`:
- Around line 306-312: unregisterProvider currently silently returns when name
is empty whereas registerProvider throws SDKException.invalidInput; change
unregisterProvider to perform the same input validation as registerProvider by
throwing SDKException.invalidInput for empty/invalid name, keep the
isNativeModuleAvailable() guard, and then proceed to delete from
registeredProviders and call
requireNativeModule().cloudUnregisterSttProvider(name); ensure you reference
SDKException.invalidInput, unregisterProvider, registerProvider,
isNativeModuleAvailable, registeredProviders.delete, and
requireNativeModule().cloudUnregisterSttProvider in the fix.

In
`@sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere`+LoRA.ts:
- Around line 550-556: The thrown SDKException in RunAnywhere+LoRA.ts when
localPath is missing uses ErrorCategory.ERROR_CATEGORY_NETWORK but this is a
storage/registry persistence problem; update the error category in the thrown
exception (in the block that checks localPath for the LoRA adapter identified by
entry.id) to a more accurate value such as ErrorCategory.ERROR_CATEGORY_STORAGE
or ErrorCategory.ERROR_CATEGORY_INTERNAL, and if cross-platform parity with the
Swift implementation is required, match the Swift choice instead.

In
`@sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere`+ModelRegistry.ts:
- Around line 785-794: The incoming parameter named model is being shadowed by
the local decoded value from ModelInfoCodec.decode(modelBytes); avoid this by
renaming one of them (e.g., rename the function parameter model to inputModel,
or rename the decoded local to registryModel) and update all usages accordingly
(notably modelForImport assignment and any subsequent references) so the intent
is clear whether you use the caller-provided ModelInfo or the registry-fetched
ModelInfo.
🪄 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: 194e3a2a-cf33-4709-b341-252cdfee216b

📥 Commits

Reviewing files that changed from the base of the PR and between f5e5460 and 7fe6364.

⛔ Files ignored due to path filters (2)
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.hpp is excluded by !**/generated/**
📒 Files selected for processing (63)
  • examples/flutter/RunAnywhereAI/lib/core/services/audio_player_service.dart
  • examples/flutter/RunAnywhereAI/lib/core/services/audio_recording_service.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/speech_to_text_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/text_to_speech_view.dart
  • examples/flutter/RunAnywhereAI/pubspec.yaml
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxproj
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx
  • sdk/runanywhere-flutter/AGENTS.md
  • sdk/runanywhere-flutter/packages/runanywhere/lib/features/stt/services/audio_capture_manager.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/features/tts/services/audio_playback_manager.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_audio.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_hybrid_stt.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_lora.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_models.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_vad.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vlm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_voice.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/events/event_bus.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/audio/audio_convert.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/hybrid/hybrid_stt_router.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere.dart
  • sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Hybrid.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.hpp
  • sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hpp
  • sdk/runanywhere-react-native/packages/core/src/Adapters/HandleStreamAdapter.ts
  • sdk/runanywhere-react-native/packages/core/src/Adapters/LLMStreamAdapter.ts
  • sdk/runanywhere-react-native/packages/core/src/Adapters/VoiceAgentStreamAdapter.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Constants/SDKConstants.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Errors/SDKException.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Errors/index.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Initialization/InitializationState.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Initialization/ServicesReadyGuard.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Logger/SDKLogger.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Services/LoggingManager.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Events/EventBus.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Audio/RunAnywhere+AudioConvert.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Embeddings/RunAnywhere+Embeddings.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/CloudSTT.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/HybridSTTRouter.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/index.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+StructuredOutput.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+ToolCalling.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelLifecycle.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/RAG/RunAnywhere+RAG.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Solutions/RunAnywhere+Solutions.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/RunAnywhere+Storage.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/RunAnywhere+VisionLanguage.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VoiceAgent/RunAnywhere+VoiceAgent.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts
  • sdk/runanywhere-react-native/packages/core/src/index.ts
  • sdk/runanywhere-react-native/packages/core/src/specs/RunAnywhereCore.nitro.ts
  • sdk/runanywhere-react-native/packages/core/tests/unit/SDKException.test.ts
💤 Files with no reviewable changes (6)
  • examples/flutter/RunAnywhereAI/lib/core/services/audio_player_service.dart
  • examples/flutter/RunAnywhereAI/lib/core/services/audio_recording_service.dart
  • examples/flutter/RunAnywhereAI/pubspec.yaml
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vlm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vad.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart
👮 Files not reviewed due to content moderation or server errors (7)
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx
  • sdk/runanywhere-react-native/packages/core/src/specs/RunAnywhereCore.nitro.ts
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Hybrid.cpp
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_models.dart
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/RAG/RunAnywhere+RAG.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelLifecycle.ts

Comment on lines +166 to +168
final urlTail = entry.url.split('/').isNotEmpty
? entry.url.split('/').last
: entry.url;

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

URL emptiness check is ineffective.

entry.url.split('/').isNotEmpty is always true because split() returns at least one element even for an empty string. If entry.url is empty, urlTail becomes an empty string, and artifactFilename could end up empty (if entry.filename is also empty).

-    final urlTail = entry.url.split('/').isNotEmpty
-        ? entry.url.split('/').last
-        : entry.url;
+    final urlParts = entry.url.split('/');
+    final urlTail = urlParts.last.isNotEmpty ? urlParts.last : entry.url;
📝 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
final urlTail = entry.url.split('/').isNotEmpty
? entry.url.split('/').last
: entry.url;
final urlParts = entry.url.split('/');
final urlTail = urlParts.last.isNotEmpty ? urlParts.last : entry.url;
🤖 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/public/capabilities/runanywhere_lora.dart`
around lines 166 - 168, The check using entry.url.split('/').isNotEmpty is
ineffective because split() always returns at least one element; instead check
entry.url.isNotEmpty (or entry.url.trim().isNotEmpty) before splitting and use
the last path segment only when the url is non-empty; assign urlTail =
entry.url.isNotEmpty ? entry.url.split('/').last : entry.filename (or a safe
fallback) and ensure artifactFilename logic uses entry.filename when both url
tail and entry.filename would otherwise be empty—update the code around the
urlTail/artifactFilename assignments (referencing variables urlTail,
artifactFilename, entry.url, entry.filename) accordingly.

Comment on lines +218 to +220
if (wavData != null && wavData.isNotEmpty) {
await _playback.play(wavData);
}

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 | 🟠 Major | ⚡ Quick win

Don’t return success when no playable audio was produced.

At Line 218, speak() silently skips playback for null/empty wavData and still returns TTSSpeakResult, which can report success with no audible output. This should fail fast.

Suggested fix
       final wavData = DartBridgeAudio.float32ToWav(
         Uint8List.fromList(output.audioData),
         sampleRate > 0 ? sampleRate : 22050,
       );

-      if (wavData != null && wavData.isNotEmpty) {
-        await _playback.play(wavData);
-      }
+      if (wavData == null || wavData.isEmpty) {
+        throw SDKException.invalidState(
+          'TTS synthesis produced no playable audio data',
+        );
+      }
+      await _playback.play(wavData);
📝 Committable suggestion

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

Suggested change
if (wavData != null && wavData.isNotEmpty) {
await _playback.play(wavData);
}
final wavData = DartBridgeAudio.float32ToWav(
Uint8List.fromList(output.audioData),
sampleRate > 0 ? sampleRate : 22050,
);
if (wavData == null || wavData.isEmpty) {
throw SDKException.invalidState(
'TTS synthesis produced no playable audio data',
);
}
await _playback.play(wavData);
🤖 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/public/capabilities/runanywhere_tts.dart`
around lines 218 - 220, In speak(), don't silently skip playback when wavData is
null/empty: add an explicit guard after generating wavData that checks if
wavData == null || wavData.isEmpty and immediately return a failing
TTSSpeakResult (success = false) with an explanatory error message instead of
proceeding to await _playback.play; keep success paths unchanged and reference
the speak() method, the wavData variable, and the _playback.play(...) call to
locate where to insert the failure return.

Comment on lines +63 to +69
static Uint8List pcm16ToWav(Uint8List int16Bytes, {required int sampleRate}) {
const pcmFormatTag = 1;
const channels = 1;
const bitsPerSample = 16;
const blockAlign = channels * bitsPerSample ~/ 8;
final byteRate = sampleRate * blockAlign;
const fmtChunkSize = 16;

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

Validate sampleRate before encoding WAV metadata.

Line 63 accepts any integer; sampleRate <= 0 produces an invalid WAV header and can cause downstream decode failures. Guard this input up front.

Suggested fix
 static Uint8List pcm16ToWav(Uint8List int16Bytes, {required int sampleRate}) {
+  if (sampleRate <= 0) {
+    throw ArgumentError.value(sampleRate, 'sampleRate', 'must be > 0');
+  }
   const pcmFormatTag = 1;
   const channels = 1;
📝 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
static Uint8List pcm16ToWav(Uint8List int16Bytes, {required int sampleRate}) {
const pcmFormatTag = 1;
const channels = 1;
const bitsPerSample = 16;
const blockAlign = channels * bitsPerSample ~/ 8;
final byteRate = sampleRate * blockAlign;
const fmtChunkSize = 16;
static Uint8List pcm16ToWav(Uint8List int16Bytes, {required int sampleRate}) {
if (sampleRate <= 0) {
throw ArgumentError.value(sampleRate, 'sampleRate', 'must be > 0');
}
const pcmFormatTag = 1;
const channels = 1;
const bitsPerSample = 16;
const blockAlign = channels * bitsPerSample ~/ 8;
final byteRate = sampleRate * blockAlign;
const fmtChunkSize = 16;
🤖 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/public/extensions/audio/audio_convert.dart`
around lines 63 - 69, The pcm16ToWav function should validate the sampleRate
input before building WAV metadata: check that sampleRate is a positive integer
(> 0) at the top of pcm16ToWav and throw a clear ArgumentError (or similar) if
it's invalid; update function pcm16ToWav to guard against sampleRate <= 0 and
return early by raising the error so invalid headers are never produced.

Comment on lines +204 to +224
subscribe(_req, onMessage, onError, onDone) {
let fan = registry.cache.get(handle);
if (!fan) {
fan = new HandleFanOut<Event>(handle, registry.options, () =>
registry.cache.delete(handle)
);
registry.cache.set(handle, fan);
}

const sub: Subscriber<Event> = { onMessage, onError, onDone };
const cancel = fan.attach(sub);
if (!cancel) {
onError(
new Error(`${registry.options.label} failed for handle ${handle}`)
);
onDone();
return () => {
/* already torn down by attach() failure */
};
}
return cancel;

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 | 🟠 Major | ⚡ Quick win

Failed fan-out registration leaves orphaned cache entry.

When fan.attach(sub) returns null (Nitro registration failed), the newly created HandleFanOut remains in registry.cache. Subsequent callers for the same handle will get the broken fan-out instance and fail immediately.

Proposed fix
         const sub: Subscriber<Event> = { onMessage, onError, onDone };
         const cancel = fan.attach(sub);
         if (!cancel) {
+          // Clean up the failed fan-out from cache
+          registry.cache.delete(handle);
           onError(
             new Error(`${registry.options.label} failed for handle ${handle}`)
           );
🤖 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-react-native/packages/core/src/Adapters/HandleStreamAdapter.ts`
around lines 204 - 224, The new HandleFanOut created in subscribe is left in
registry.cache when fan.attach(sub) fails, causing future callers to get a
broken fan; update subscribe so that after creating and setting fan in
registry.cache, if fan.attach(sub) returns null you call
registry.cache.delete(handle) (or otherwise remove the newly inserted
HandleFanOut) before invoking onError/onDone and returning, ensuring no orphaned
cache entry remains; reference the subscribe function, registry.cache,
HandleFanOut constructor, fan.attach, and the cancel/null check to locate where
to remove the cache entry.

Comment on lines +244 to +252
for (const [key, value] of Object.entries(metadata)) {
if (shouldRedact(key)) {
sanitized[key] = '[REDACTED]';
} else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
sanitized[key] = sanitizeMetadata(value as Record<string, unknown>) ?? {};
} else {
sanitized[key] = value;
}
}

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 | 🟠 Major | ⚡ Quick win

Arrays containing objects with sensitive keys are not sanitized.

The current implementation skips arrays (!Array.isArray(value)), so metadata like { users: [{ token: 'secret' }] } will leak the nested token value to log destinations.

Proposed fix to handle arrays recursively
     if (shouldRedact(key)) {
       sanitized[key] = '[REDACTED]';
     } else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
       sanitized[key] = sanitizeMetadata(value as Record<string, unknown>) ?? {};
+    } else if (Array.isArray(value)) {
+      sanitized[key] = value.map((item) =>
+        item !== null && typeof item === 'object' && !Array.isArray(item)
+          ? sanitizeMetadata(item as Record<string, unknown>) ?? {}
+          : item
+      );
     } else {
       sanitized[key] = value;
     }
📝 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
for (const [key, value] of Object.entries(metadata)) {
if (shouldRedact(key)) {
sanitized[key] = '[REDACTED]';
} else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
sanitized[key] = sanitizeMetadata(value as Record<string, unknown>) ?? {};
} else {
sanitized[key] = value;
}
}
for (const [key, value] of Object.entries(metadata)) {
if (shouldRedact(key)) {
sanitized[key] = '[REDACTED]';
} else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
sanitized[key] = sanitizeMetadata(value as Record<string, unknown>) ?? {};
} else if (Array.isArray(value)) {
sanitized[key] = value.map((item) =>
item !== null && typeof item === 'object' && !Array.isArray(item)
? sanitizeMetadata(item as Record<string, unknown>) ?? {}
: item
);
} else {
sanitized[key] = value;
}
}
🤖 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-react-native/packages/core/src/Foundation/Logging/Services/LoggingManager.ts`
around lines 244 - 252, The sanitizeMetadata implementation currently skips
arrays, so nested objects inside arrays (e.g., users: [{ token: 'secret' }])
aren't redacted; update the loop in sanitizeMetadata to detect
Array.isArray(value) and, when true, produce sanitized[key] = value.map(elem =>
(elem !== null && typeof elem === 'object' && !Array.isArray(elem)) ?
sanitizeMetadata(elem as Record<string, unknown>) ?? {} : elem) (and also handle
nested arrays recursively if needed), while preserving the existing
shouldRedact(key) check and behavior for non-object values.

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

Caution

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

⚠️ Outside diff range comments (2)
sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Voice.cpp (1)

528-540: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Cancel the live STT stream before unloading the STT component.

unloadSTTModel() now bypasses the session teardown path added below. If a stream is active, g_stt_stream_session keeps a live sessionId/callback/handle after rac_stt_component_cleanup(handle), so later callbacks or sttStreamStop() / sttStreamCancel() operate on torn-down native state. Reuse the same takeSTTStreamSession()cancelSTTStreamSession()g_stt_stream_loaded_model_id.clear() sequence that resetAllGlobalComponentHandles() already uses before cleaning the component.

🤖 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-react-native/packages/core/cpp/HybridRunAnywhereCore`+Voice.cpp
around lines 528 - 540, unloadSTTModel currently cleans up the STT component
directly, which can leave an active stream session dangling; before calling
rac_stt_component_cleanup(handle) in HybridRunAnywhereCore::unloadSTTModel(),
acquire and clear any live stream session using takeSTTStreamSession() then call
cancelSTTStreamSession() and clear g_stt_stream_loaded_model_id (mirroring
resetAllGlobalComponentHandles()), ensuring g_stt_stream_session is properly
torn down under g_stt_mutex protection, then proceed to
rac_stt_component_cleanup(handle) and null out g_stt_component_handle.
examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx (1)

51-58: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Restore the required platform-specific TTS implementations on this screen.

This change routes both iOS and Android through RunAnywhere.speak() only. For TTSScreen, the repo contract is the opposite: iOS should use NativeModules.NativeAudioModule and Android should lazy-load react-native-tts, so the sample no longer exercises the per-platform TTS stack it is supposed to cover.

As per coding guidelines, examples/react-native/RunAnywhereAI/**/{TTSScreen,VoiceAssistantScreen}.tsx: “Import platform-specific TTS implementations: use NativeModules.NativeAudioModule (AVSpeechSynthesizer) on iOS and lazy-loaded react-native-tts on Android”.

Also applies to: 223-275

🤖 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/TTSScreen.tsx` around lines
51 - 58, The TTSScreen (and VoiceAssistantScreen) currently routes iOS and
Android through RunAnywhere.speak(), but must restore platform-specific TTS: on
iOS call NativeModules.NativeAudioModule (AVSpeechSynthesizer) instead of
RunAnywhere.speak(), and on Android lazy-load and use react-native-tts (dynamic
import) rather than RunAnywhere.speak(); update the TTSScreen component and any
helper functions to import/use NativeModules.NativeAudioModule for iOS, perform
a dynamic import('react-native-tts') and use its speak method on Android, and
keep RunAnywhere.speak() only for non-mobile or fallback cases so the sample
exercises the per-platform TTS stack as required.

Source: Coding guidelines

🧹 Nitpick comments (4)
sdk/runanywhere-react-native/packages/core/ios/HybridAudioPlayback.swift (1)

219-226: 💤 Low value

Use bitwise check for interruption options flags.

The flags parameter is an OptionSet raw value that could contain multiple bits. Using exact equality works today (only .shouldResume exists), but bitwise AND is more robust for forward compatibility.

♻️ Suggested improvement
     fileprivate func endInterruption(player: AVAudioPlayer, withOptions flags: Int) {
         // Swift SDK AudioPlaybackManager.swift:246-254 — auto-resume on
         // .shouldResume.
         logger.info("Playback interruption ended")
-        if flags == AVAudioSession.InterruptionOptions.shouldResume.rawValue {
+        let shouldResume = AVAudioSession.InterruptionOptions.shouldResume.rawValue
+        if (flags & shouldResume) == shouldResume {
             player.play()
         }
     }
🤖 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-react-native/packages/core/ios/HybridAudioPlayback.swift`
around lines 219 - 226, The endInterruption(player:withOptions:) function uses
exact equality on the OptionSet raw value; change the check to a bitwise
membership test against AVAudioSession.InterruptionOptions.shouldResume.rawValue
(e.g., test (flags & shouldResume.rawValue) != 0) so the code correctly detects
the shouldResume bit when multiple interruption option bits are present and then
call player.play().
sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_stt.dart (1)

190-194: 💤 Low value

Catch-all exception handling may mask unexpected errors.

The catch (_) swallows all exceptions from ensureServicesReady(), treating them uniformly as "not ready" states. If ensureServicesReady() throws due to an internal bug or unexpected condition (not just "services not initialized"), the error will be silently discarded.

Consider catching a more specific exception type or logging the error before the silent return for debuggability.

💡 Optional: Log before silent return
          try {
            await DartBridge.ensureServicesReady();
          } catch (_) {
+           // Consider: SDKLogger('RunAnywhereSTT').debug('Services not ready, finishing stream silently');
            return; // Silent finish (Swift parity).
          }
🤖 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/public/capabilities/runanywhere_stt.dart`
around lines 190 - 194, The try/catch around DartBridge.ensureServicesReady() is
swallowing all exceptions (catch (_)) which can hide real errors; update the
handler to either catch a more specific exception type thrown by
ensureServicesReady() or capture and log the error (and stack trace) before
returning so unexpected failures are visible during debugging; locate the try
block that calls DartBridge.ensureServicesReady() and replace the broad catch
with a specific catch or a catch (e, st) that logs the error context (using the
package's logging mechanism) and then returns silently only for known "not
ready" conditions.
sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_downloads.dart (2)

259-266: 💤 Low value

Incorrect error category for missing local path.

The error category ERROR_CATEGORY_NETWORK is semantically incorrect for a missing local_path condition. This is a data/state issue, not a network issue. Consider using ERROR_CATEGORY_INTERNAL or ERROR_CATEGORY_STORAGE if available.

🔧 Suggested fix
     if (localPath.isEmpty) {
       throw SDKException.make(
         code: ErrorCode.ERROR_CODE_INVALID_STATE,
         message: 'Download completed without a local_path; cannot import '
             'completion into the model registry',
-        category: ErrorCategory.ERROR_CATEGORY_NETWORK,
+        category: ErrorCategory.ERROR_CATEGORY_INTERNAL,
       );
     }
🤖 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/public/capabilities/runanywhere_downloads.dart`
around lines 259 - 266, The error thrown when localPath.isEmpty uses the wrong
category: update the SDKException.make call in the block that checks localPath
(the localPath.isEmpty guard) to use a non-network category (e.g.,
ErrorCategory.ERROR_CATEGORY_INTERNAL or ERROR_CATEGORY_STORAGE if defined)
instead of ErrorCategory.ERROR_CATEGORY_NETWORK so the exception correctly
represents a data/state problem.

286-294: 💤 Low value

Same category issue for import failure.

The ERROR_CATEGORY_NETWORK is also used here for an import failure, which is a registry/storage operation, not a network operation.

🔧 Suggested fix
     if (!result.success) {
       throw SDKException.make(
         code: ErrorCode.ERROR_CODE_DOWNLOAD_FAILED,
         message: result.errorMessage.isEmpty
             ? 'Downloaded model could not be imported into the registry'
             : result.errorMessage,
-        category: ErrorCategory.ERROR_CATEGORY_NETWORK,
+        category: ErrorCategory.ERROR_CATEGORY_STORAGE,
       );
     }
🤖 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/public/capabilities/runanywhere_downloads.dart`
around lines 286 - 294, The code throws an SDKException with
ErrorCode.ERROR_CODE_DOWNLOAD_FAILED but incorrectly sets
ErrorCategory.ERROR_CATEGORY_NETWORK for an import/registry failure; update the
exception in the block that checks result.success (where result.errorMessage is
used and SDKException.make is called) to use the registry/storage category
instead (e.g., ErrorCategory.ERROR_CATEGORY_REGISTRY) so the error category
reflects a registry/storage import failure rather than a network error.
🤖 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/react-native/RunAnywhereAI/src/screens/STTScreen.tsx`:
- Line 54: The change incorrectly imports and uses AudioCaptureManager
universally, which bypasses required platform-specific STT paths; revert to
platform-specific handling in STTScreen (and similarly in VoiceAssistantScreen)
by using NativeAudioModule.startRecording() on iOS and the native recorder +
RunAnywhere.transcribe() on Android—restore conditional imports/usage around
AudioCaptureManager, NativeAudioModule.startRecording, and
RunAnywhere.transcribe so iOS uses AVFoundation via NativeAudioModule and
Android uses RunAnywhere.transcribe with the native recorder path.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/model_category_extensions.dart`:
- Around line 20-26: The defaultFramework getter currently returns
INFERENCE_FRAMEWORK_UNKNOWN when
RacNative.bindings.rac_model_category_default_framework is missing; instead,
when fn == null restore the previous per-category Dart fallback logic (the
original switch on ModelCategory) used before this change, and only call
fn(toC()) and pass its result to inferenceFrameworkFromC when the symbol exists;
update the getter to check fn, run the original Dart switch fallback for each
ModelCategory case when fn is null, and delegate to the native binding otherwise
(preserve use of inferenceFrameworkFromC and toC()).

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_logging.dart`:
- Around line 35-42: The public API lost the LogLevel enum re-export causing
callers of RunAnywhereLogging.setLogLevel() to fail; re-export the LogLevel enum
from package:runanywhere/generated/logging.pb.dart so callers importing
runanywhere_logging.dart can reference LogLevel (e.g., include LogLevel in the
show list alongside LogEntry and LoggingConfiguration) and ensure
RunAnywhereLogging.setLogLevel() continues to compile for downstream users.

In
`@sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/HybridAudioCapture.kt`:
- Around line 180-200: The read-loop can exit on AudioRecord.read() returning a
negative error but currently only resets currentAudioLevel, leaking
recordingFlag, audioRecord and audio focus so isRecording stays true; update the
bytesRead < 0 branch inside the captureJob (the coroutine launched where
captureJob is assigned and record.read is called) to perform the same teardown
as stopRecording() (clear recordingFlag, release/stop audioRecord, abandon audio
focus and null out audioRecord references) instead of just breaking, or invoke
the existing stopRecording() cleanup path safely (ensure you don't deadlock if
stopRecording() interacts with this coroutine) so the recorder and focus are
fully released and isRecording becomes false on non-happy path exits.

In `@sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp`:
- Around line 1849-1857: The current logic sets info.hasNeuralEngine based
solely on info.architecture ("arm64"), which incorrectly marks Apple simulators
as having a Neural Engine; update the check in InitBridge.cpp so
info.hasNeuralEngine is true only when architecture == "arm64" AND the process
is not a simulator (i.e., add an explicit simulator guard instead of relying
only on architecture). Locate and modify the block that sets
info.hasNeuralEngine and info.neuralEngineCores (referencing symbols
info.hasNeuralEngine, info.architecture, and info.neuralEngineCores) to query a
simulator indicator (e.g., a platform API or
TARGET_OS_SIMULATOR/PlatformAdapter_isSimulator equivalent) and set
neuralEngineCores = info.hasNeuralEngine ? 16 : 0.

In `@sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.hpp`:
- Around line 228-239: The STT session ID must not be round-tripped through
double/JS number; change the API to return and accept an opaque 64-bit-safe
handle (e.g., std::string token or BigInt-backed string) instead of
Promise<double> and double parameters: update sttStreamStart to return
std::shared_ptr<Promise<std::string>> (or another opaque handle type) and change
sttStreamFeed/sttStreamStop/sttStreamCancel to take const std::string&
sessionId; then update the conversions in HybridRunAnywhereCore+Voice.cpp to
pass the native uint64_t session id unchanged by encoding it to the chosen
opaque form (e.g., decimal hex string or UUID) and decode it back to uint64_t on
the native side, and align consumers (e.g., nitro/front-end) to treat the handle
as a non-number/BigInt token so 64-bit values are preserved.

---

Outside diff comments:
In `@examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx`:
- Around line 51-58: The TTSScreen (and VoiceAssistantScreen) currently routes
iOS and Android through RunAnywhere.speak(), but must restore platform-specific
TTS: on iOS call NativeModules.NativeAudioModule (AVSpeechSynthesizer) instead
of RunAnywhere.speak(), and on Android lazy-load and use react-native-tts
(dynamic import) rather than RunAnywhere.speak(); update the TTSScreen component
and any helper functions to import/use NativeModules.NativeAudioModule for iOS,
perform a dynamic import('react-native-tts') and use its speak method on
Android, and keep RunAnywhere.speak() only for non-mobile or fallback cases so
the sample exercises the per-platform TTS stack as required.

In
`@sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore`+Voice.cpp:
- Around line 528-540: unloadSTTModel currently cleans up the STT component
directly, which can leave an active stream session dangling; before calling
rac_stt_component_cleanup(handle) in HybridRunAnywhereCore::unloadSTTModel(),
acquire and clear any live stream session using takeSTTStreamSession() then call
cancelSTTStreamSession() and clear g_stt_stream_loaded_model_id (mirroring
resetAllGlobalComponentHandles()), ensuring g_stt_stream_session is properly
torn down under g_stt_mutex protection, then proceed to
rac_stt_component_cleanup(handle) and null out g_stt_component_handle.

---

Nitpick comments:
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_downloads.dart`:
- Around line 259-266: The error thrown when localPath.isEmpty uses the wrong
category: update the SDKException.make call in the block that checks localPath
(the localPath.isEmpty guard) to use a non-network category (e.g.,
ErrorCategory.ERROR_CATEGORY_INTERNAL or ERROR_CATEGORY_STORAGE if defined)
instead of ErrorCategory.ERROR_CATEGORY_NETWORK so the exception correctly
represents a data/state problem.
- Around line 286-294: The code throws an SDKException with
ErrorCode.ERROR_CODE_DOWNLOAD_FAILED but incorrectly sets
ErrorCategory.ERROR_CATEGORY_NETWORK for an import/registry failure; update the
exception in the block that checks result.success (where result.errorMessage is
used and SDKException.make is called) to use the registry/storage category
instead (e.g., ErrorCategory.ERROR_CATEGORY_REGISTRY) so the error category
reflects a registry/storage import failure rather than a network error.

In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_stt.dart`:
- Around line 190-194: The try/catch around DartBridge.ensureServicesReady() is
swallowing all exceptions (catch (_)) which can hide real errors; update the
handler to either catch a more specific exception type thrown by
ensureServicesReady() or capture and log the error (and stack trace) before
returning so unexpected failures are visible during debugging; locate the try
block that calls DartBridge.ensureServicesReady() and replace the broad catch
with a specific catch or a catch (e, st) that logs the error context (using the
package's logging mechanism) and then returns silently only for known "not
ready" conditions.

In `@sdk/runanywhere-react-native/packages/core/ios/HybridAudioPlayback.swift`:
- Around line 219-226: The endInterruption(player:withOptions:) function uses
exact equality on the OptionSet raw value; change the check to a bitwise
membership test against AVAudioSession.InterruptionOptions.shouldResume.rawValue
(e.g., test (flags & shouldResume.rawValue) != 0) so the code correctly detects
the shouldResume bit when multiple interruption option bits are present and then
call player.play().
🪄 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: 4509f6d5-6e02-4462-8dfe-d79399a35efd

📥 Commits

Reviewing files that changed from the base of the PR and between 7fe6364 and 1e115b0.

⛔ Files ignored due to path filters (31)
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JHybridAudioCaptureSpec.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JHybridAudioCaptureSpec.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JHybridAudioPlaybackSpec.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JHybridAudioPlaybackSpec.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/kotlin/com/margelo/nitro/runanywhere/Func_void_std__shared_ptr_ArrayBuffer_.kt is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/kotlin/com/margelo/nitro/runanywhere/HybridAudioCaptureSpec.kt is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/kotlin/com/margelo/nitro/runanywhere/HybridAudioPlaybackSpec.kt is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/runanywherecore+autolinking.cmake is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/runanywherecoreOnLoad.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCore-Swift-Cxx-Bridge.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCore-Swift-Cxx-Bridge.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCore-Swift-Cxx-Umbrella.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCoreAutolinking.mm is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCoreAutolinking.swift is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/c++/HybridAudioCaptureSpecSwift.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/c++/HybridAudioCaptureSpecSwift.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/c++/HybridAudioPlaybackSpecSwift.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/c++/HybridAudioPlaybackSpecSwift.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/Func_void.swift is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/HybridAudioCaptureSpec.swift is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/HybridAudioCaptureSpec_cxx.swift is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/HybridAudioPlaybackSpec.swift is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/HybridAudioPlaybackSpec_cxx.swift is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridAudioCaptureSpec.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridAudioCaptureSpec.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridAudioPlaybackSpec.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridAudioPlaybackSpec.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.hpp is excluded by !**/generated/**
📒 Files selected for processing (105)
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxproj
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI/NativeAudioModule.m
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI/NativeAudioModule.swift
  • examples/react-native/RunAnywhereAI/knip.json
  • examples/react-native/RunAnywhereAI/package.json
  • examples/react-native/RunAnywhereAI/react-native.config.js
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx
  • examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts
  • sdk/runanywhere-commons/include/rac/infrastructure/events/rac_sdk_event_stream.h
  • sdk/runanywhere-commons/src/core/rac_core.cpp
  • sdk/runanywhere-commons/src/core/sdk_state.cpp
  • sdk/runanywhere-commons/src/infrastructure/events/event_publisher.cpp
  • sdk/runanywhere-commons/src/lifecycle/sdk_init.cpp
  • sdk/runanywhere-commons/src/router/hybrid/rac_stt_hybrid_router_proto.cpp
  • sdk/runanywhere-commons/tests/test_sdk_event_stream.cpp
  • sdk/runanywhere-flutter/packages/runanywhere/lib/core/module/runanywhere_module.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/core/native/rac_native.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/foundation/constants/sdk_constants.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/foundation/logging/sdk_logger.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_tts.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/type_conversions/model_types_cpp_bridge.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_downloads.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_llm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_lora.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_models.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_plugin_loader.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_vad.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vlm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_voice.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/model_category_extensions.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/rag_module.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_logging.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_structured_output.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/hybrid/hybrid_stt_router.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart
  • sdk/runanywhere-flutter/packages/runanywhere_genie/lib/genie.dart
  • sdk/runanywhere-flutter/packages/runanywhere_llamacpp/lib/llamacpp.dart
  • sdk/runanywhere-flutter/packages/runanywhere_onnx/lib/onnx.dart
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/HybridSTTRouter.kt
  • sdk/runanywhere-react-native/packages/core/README.md
  • sdk/runanywhere-react-native/packages/core/android/src/main/AndroidManifest.xml
  • sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/HybridAudioCapture.kt
  • sdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/HybridAudioPlayback.kt
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+ProtoCompat.hpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Registry.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Storage.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Voice.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.hpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridVoiceAgent.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cpp
  • sdk/runanywhere-react-native/packages/core/ios/HybridAudioCapture.swift
  • sdk/runanywhere-react-native/packages/core/ios/HybridAudioPlayback.swift
  • sdk/runanywhere-react-native/packages/core/nitro.json
  • sdk/runanywhere-react-native/packages/core/src/Features/VoiceSession/AudioCaptureManager.ts
  • sdk/runanywhere-react-native/packages/core/src/Features/VoiceSession/AudioPlaybackManager.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Errors/SDKException.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Initialization/InitializedGuard.ts
  • sdk/runanywhere-react-native/packages/core/src/Foundation/Initialization/index.ts
  • sdk/runanywhere-react-native/packages/core/src/Internal/Nitro/NitroAudioCaptureSpec.ts
  • sdk/runanywhere-react-native/packages/core/src/Internal/Nitro/NitroAudioPlaybackSpec.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Embeddings/EmbeddingsProto+Helpers.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Embeddings/RunAnywhere+Embeddings.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/HybridDeviceState.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/HybridSTTRouter.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+StructuredOutput.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+TextGeneration.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+ToolCalling.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelLifecycle.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/RAG/RunAnywhere+RAG.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/STT/RunAnywhere+STT.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Solutions/RunAnywhere+Solutions.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/RunAnywhere+Storage.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/StorageProto+Helpers.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/TTS/RunAnywhere+TTS.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VAD/RunAnywhere+VAD.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/RunAnywhere+VisionLanguage.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/VLMImage+Helpers.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Extensions/VoiceAgent/RunAnywhere+VoiceAgent.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Helpers/SDKComponent+DisplayName.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Helpers/SDKEnvironment+Helpers.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/Helpers/formatFramework.ts
  • sdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.ts
  • sdk/runanywhere-react-native/packages/core/src/index.ts
  • sdk/runanywhere-react-native/packages/core/src/specs/AudioCapture.nitro.ts
  • sdk/runanywhere-react-native/packages/core/src/specs/AudioPlayback.nitro.ts
  • sdk/runanywhere-react-native/packages/core/src/specs/RunAnywhereCore.nitro.ts
  • sdk/runanywhere-react-native/packages/onnx/src/ONNX.ts
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Telemetry.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Constants/SDKConstants.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Security/KeychainManager.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swift
  • thoughts/shared/plans/rn_flutter_strict_swift_parity.md
💤 Files with no reviewable changes (7)
  • sdk/runanywhere-flutter/packages/runanywhere/lib/core/module/runanywhere_module.dart
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI/NativeAudioModule.swift
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI/NativeAudioModule.m
  • examples/react-native/RunAnywhereAI/package.json
  • examples/react-native/RunAnywhereAI/react-native.config.js
  • examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxproj
✅ Files skipped from review due to trivial changes (6)
  • sdk/runanywhere-react-native/packages/core/android/src/main/AndroidManifest.xml
  • sdk/runanywhere-react-native/packages/core/README.md
  • examples/react-native/RunAnywhereAI/knip.json
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_tts.dart
  • sdk/runanywhere-react-native/packages/core/nitro.json
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/HybridSTTRouter.kt
🚧 Files skipped from review as they are similar to previous changes (5)
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/hybrid/hybrid_stt_router.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vlm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_models.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tts.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart


// Import RunAnywhere SDK (Multi-Package Architecture)
import { RunAnywhere } from '@runanywhere/core';
import { RunAnywhere, AudioCaptureManager } from '@runanywhere/core';

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 | 🟠 Major | 🏗️ Heavy lift

This bypasses the required platform-specific STT capture paths.

STTScreen now hard-wires AudioCaptureManager for both platforms. This sample is supposed to use the iOS AVFoundation path via NativeAudioModule.startRecording() and the Android native recorder path with RunAnywhere.transcribe(), so this change removes the platform-specific behavior the repo explicitly requires.

As per coding guidelines, examples/react-native/RunAnywhereAI/**/{STTScreen,VoiceAssistantScreen}.tsx: “Import platform-specific STT recording: use NativeAudioModule.startRecording() (AVFoundation) on iOS and native audio recorder with RunAnywhere.transcribe() on Android”.

Also applies to: 137-145, 404-418

🤖 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` at line 54,
The change incorrectly imports and uses AudioCaptureManager universally, which
bypasses required platform-specific STT paths; revert to platform-specific
handling in STTScreen (and similarly in VoiceAssistantScreen) by using
NativeAudioModule.startRecording() on iOS and the native recorder +
RunAnywhere.transcribe() on Android—restore conditional imports/usage around
AudioCaptureManager, NativeAudioModule.startRecording, and
RunAnywhere.transcribe so iOS uses AVFoundation via NativeAudioModule and
Android uses RunAnywhere.transcribe with the native recorder path.

Source: Coding guidelines

Comment on lines 20 to +26
InferenceFramework get defaultFramework {
switch (this) {
case ModelCategory.MODEL_CATEGORY_LANGUAGE:
case ModelCategory.MODEL_CATEGORY_MULTIMODAL:
return InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP;
case ModelCategory.MODEL_CATEGORY_SPEECH_RECOGNITION:
case ModelCategory.MODEL_CATEGORY_SPEECH_SYNTHESIS:
case ModelCategory.MODEL_CATEGORY_EMBEDDING:
case ModelCategory.MODEL_CATEGORY_VOICE_ACTIVITY_DETECTION:
return InferenceFramework.INFERENCE_FRAMEWORK_ONNX;
default:
return InferenceFramework.INFERENCE_FRAMEWORK_UNKNOWN;
final fn = RacNative.bindings.rac_model_category_default_framework;
if (fn == null) {
return InferenceFramework.INFERENCE_FRAMEWORK_UNKNOWN;
}
return inferenceFrameworkFromC(fn(toC()));
}

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 | 🟠 Major | ⚡ Quick win

Preserve the old per-category fallback when the symbol is missing.

rac_model_category_default_framework is looked up optionally, so older commons binaries are still a supported runtime here. Falling back to INFERENCE_FRAMEWORK_UNKNOWN drops deterministic framework resolution exactly on that compatibility path. Keep the previous Dart switch as the fn == null fallback and only delegate to commons when the symbol exists.

Based on learnings from the diff summary, this getter replaced the previous Dart switch fallback in this PR.

🤖 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/public/extensions/model_category_extensions.dart`
around lines 20 - 26, The defaultFramework getter currently returns
INFERENCE_FRAMEWORK_UNKNOWN when
RacNative.bindings.rac_model_category_default_framework is missing; instead,
when fn == null restore the previous per-category Dart fallback logic (the
original switch on ModelCategory) used before this change, and only call
fn(toC()) and pass its result to inferenceFrameworkFromC when the symbol exists;
update the getter to check fn, run the original Dart switch fallback for each
ModelCategory case when fn is null, and delegate to the native binding otherwise
(preserve use of inferenceFrameworkFromC and toC()).

Comment on lines +35 to +42
export 'package:runanywhere/foundation/logging/sdk_logger.dart'
show
ConsoleLogDestination,
LogDestination,
LoggingConfigurations,
SDKLoggerConfig;
export 'package:runanywhere/generated/logging.pb.dart'
show LogEntry, LoggingConfiguration;

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 | 🟠 Major | ⚡ Quick win

Re-export LogLevel here or the public API breaks.

RunAnywhereLogging.setLogLevel() still takes LogLevel, but this file no longer re-exports that enum. Callers importing only runanywhere_logging.dart can no longer spell LogLevel.LOG_LEVEL_*, which is a source-compatible break.

Patch
 export 'package:runanywhere/generated/logging.pb.dart'
-    show LogEntry, LoggingConfiguration;
+    show LogEntry, LoggingConfiguration, LogLevel;
📝 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
export 'package:runanywhere/foundation/logging/sdk_logger.dart'
show
ConsoleLogDestination,
LogDestination,
LoggingConfigurations,
SDKLoggerConfig;
export 'package:runanywhere/generated/logging.pb.dart'
show LogEntry, LoggingConfiguration;
export 'package:runanywhere/foundation/logging/sdk_logger.dart'
show
ConsoleLogDestination,
LogDestination,
LoggingConfigurations,
SDKLoggerConfig;
export 'package:runanywhere/generated/logging.pb.dart'
show LogEntry, LoggingConfiguration, LogLevel;
🤖 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/public/extensions/runanywhere_logging.dart`
around lines 35 - 42, The public API lost the LogLevel enum re-export causing
callers of RunAnywhereLogging.setLogLevel() to fail; re-export the LogLevel enum
from package:runanywhere/generated/logging.pb.dart so callers importing
runanywhere_logging.dart can reference LogLevel (e.g., include LogLevel in the
show list alongside LogEntry and LoggingConfiguration) and ensure
RunAnywhereLogging.setLogLevel() continues to compile for downstream users.

Comment on lines +180 to +200
captureJob = scope.launch {
val buffer = ByteArray(chunkBytes)
try {
while (isActive && recordingFlag.get()) {
val bytesRead = record.read(buffer, 0, chunkBytes)
if (bytesRead > 0) {
val chunk = buffer.copyOf(bytesRead)
currentAudioLevel = computeNormalizedLevel(chunk)
try {
onAudioData(ArrayBuffer.copy(chunk))
} catch (t: Throwable) {
logger.error("onAudioData callback threw: ${t.message}")
}
} else if (bytesRead < 0) {
logger.warning("AudioRecord.read error: $bytesRead — stopping capture")
break
}
}
} finally {
currentAudioLevel = 0.0
}

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 | 🟠 Major | ⚡ Quick win

Unexpected read-loop exit leaves capture stuck in the recording state.

When AudioRecord.read() returns a negative error and the loop breaks, the coroutine only zeroes currentAudioLevel. recordingFlag, audioRecord, and audio focus stay live, so isRecording remains true and every later startRecording() bails out as "Already recording" while the recorder/focus are leaked. Fold the same teardown used by stopRecording() into the non-happy-path exit of this coroutine.

🤖 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-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/HybridAudioCapture.kt`
around lines 180 - 200, The read-loop can exit on AudioRecord.read() returning a
negative error but currently only resets currentAudioLevel, leaking
recordingFlag, audioRecord and audio focus so isRecording stays true; update the
bytesRead < 0 branch inside the captureJob (the coroutine launched where
captureJob is assigned and record.read is called) to perform the same teardown
as stopRecording() (clear recordingFlag, release/stop audioRecord, abandon audio
focus and null out audioRecord references) instead of just breaking, or invoke
the existing stopRecording() cleanup path safely (ensure you don't deadlock if
stopRecording() interacts with this coroutine) so the recorder and focus are
fully released and isRecording becomes false on non-happy path exits.

Comment on lines +1849 to +1857
// Mirrors Swift DeviceInfo.swift: Neural Engine is derived from the
// architecture (arm64 Apple silicon), never hardcoded — x86 simulators
// report none. Cores follow Swift's `hasNeuralEngine ? 16 : 0`.
#if defined(__APPLE__)
info.hasNeuralEngine = info.architecture == "arm64";
#else
info.hasNeuralEngine = false;
#endif
info.neuralEngineCores = info.hasNeuralEngine ? 16 : 0;

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 | 🟠 Major | ⚡ Quick win

arm64 alone overstates Apple simulator capabilities.

On Apple Silicon iOS simulators, PlatformAdapter_getArchitecture() is also arm64, so this marks simulators as having a Neural Engine with 16 cores. That incorrect capability data is then sent through device registration. Gate this with an explicit simulator check, or source the signal from a platform API that excludes simulators.

🤖 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-react-native/packages/core/cpp/bridges/InitBridge.cpp` around
lines 1849 - 1857, The current logic sets info.hasNeuralEngine based solely on
info.architecture ("arm64"), which incorrectly marks Apple simulators as having
a Neural Engine; update the check in InitBridge.cpp so info.hasNeuralEngine is
true only when architecture == "arm64" AND the process is not a simulator (i.e.,
add an explicit simulator guard instead of relying only on architecture). Locate
and modify the block that sets info.hasNeuralEngine and info.neuralEngineCores
(referencing symbols info.hasNeuralEngine, info.architecture, and
info.neuralEngineCores) to query a simulator indicator (e.g., a platform API or
TARGET_OS_SIMULATOR/PlatformAdapter_isSimulator equivalent) and set
neuralEngineCores = info.hasNeuralEngine ? 16 : 0.

Comment on lines +228 to +239
std::shared_ptr<Promise<bool>> sttStreamLoadModel(
const std::string& modelPath,
const std::string& modelId,
const std::string& modelName) override;
std::shared_ptr<Promise<double>> sttStreamStart(
const std::shared_ptr<ArrayBuffer>& optionsBytes,
const std::function<void(const std::shared_ptr<ArrayBuffer>&)>& onEventBytes) override;
std::shared_ptr<Promise<void>> sttStreamFeed(
double sessionId,
const std::shared_ptr<ArrayBuffer>& audioBytes) override;
std::shared_ptr<Promise<void>> sttStreamStop(double sessionId) override;
std::shared_ptr<Promise<void>> sttStreamCancel(double sessionId) override;

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a 'RunAnywhereCore\.nitro\.ts$|HybridRunAnywhereCore\.hpp$|rac_native\.dart$' . | while read -r f; do
  echo "---- $f"
  rg -n -C2 'sttStream(Start|Feed|Stop|Cancel)|Uint64' "$f"
done

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 3829


🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a 'HybridRunAnywhereCore\+Voice\.(cpp|cc|cxx|h|hpp)$' . | while read -r f; do
  echo "---- $f"
  rg -n -C3 'sttStream(Start|Feed|Stop|Cancel)' "$f" || true
  rg -n -C3 'uint64_t.*sessionId|sessionId.*uint64_t|static_cast<\s*double\s*>\(|static_cast<\s*uint64_t\s*>\(' "$f" || true
done

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 6252


Don’t round-trip STT stream session IDs through double/JS number.

HybridRunAnywhereCore.hpp (228-239) exposes sttStreamStart() as Promise<double> and sttStreamFeed/Stop/Cancel(sessionId: double). HybridRunAnywhereCore+Voice.cpp converts the native uint64_t sessionId to double (return static_cast<double>(sessionId)) and back (static_cast<uint64_t>(sessionId)), which will corrupt session tokens once they exceed 2^53. The Nitro spec models these as JS number (RunAnywhereCore.nitro.ts: 493-517), while Flutter’s rac_native.dart preserves the session id as ffi.Uint64.

Use an opaque handle that’s safe for full 64-bit values (e.g., BigInt-backed or a non-numeric token), and keep the native uint64_t representation intact across the boundary.

🤖 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-react-native/packages/core/cpp/HybridRunAnywhereCore.hpp`
around lines 228 - 239, The STT session ID must not be round-tripped through
double/JS number; change the API to return and accept an opaque 64-bit-safe
handle (e.g., std::string token or BigInt-backed string) instead of
Promise<double> and double parameters: update sttStreamStart to return
std::shared_ptr<Promise<std::string>> (or another opaque handle type) and change
sttStreamFeed/sttStreamStop/sttStreamCancel to take const std::string&
sessionId; then update the conversions in HybridRunAnywhereCore+Voice.cpp to
pass the native uint64_t session id unchanged by encoding it to the chosen
opaque form (e.g., decimal hex string or UUID) and decode it back to uint64_t on
the native side, and align consumers (e.g., nitro/front-end) to treat the handle
as a non-number/BigInt token so 64-bit values are preserved.

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

Caution

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

⚠️ Outside diff range comments (2)
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx (1)

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

Remove console.log statement per coding guidelines.

Same issue — only console.warn and console.error are permitted.

Proposed fix
-      // eslint-disable-next-line no-console -- demo generation diagnostic
-      console.log(
-        '[ChatScreen] Starting streaming generation for:',
-        prompt,
-        'model:',
-        currentModel?.id
-      );
🤖 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
377 - 383, Remove the console.log call in ChatScreen.tsx that prints
"[ChatScreen] Starting streaming generation for:" (which references prompt and
currentModel?.id); either delete the entire logging statement and the
accompanying eslint-disable-next-line comment, or replace it with console.warn
or console.error per guidelines (e.g., console.warn('[ChatScreen] Starting
streaming generation for:', prompt, 'model:', currentModel?.id)); update the
surrounding code in the ChatScreen component/streaming generation handler
accordingly.

Source: Coding guidelines

examples/react-native/RunAnywhereAI/src/services/VLMService.ts (1)

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

Missing EOS token stripping per coding guidelines.

The coding guidelines specify: "Strip EOS tokens from VLM model output in VLMService before returning results." The refactored processImage streams tokens directly via onToken(event.token) without sanitizing common end-of-sequence markers (<|end|>, <|endoftext|>, </s>, etc.), which will appear in the UI.

🛡️ Suggested fix

Add a helper method and apply it before calling onToken:

+  private stripEosTokens(text: string): string {
+    return text.replace(/<\|end\|>|<\|endoftext\|>|<\/s>|<eos>/g, '');
+  }
+
   async processImage(
     imagePath: string,
     prompt: string,
     maxTokens: number,
     onToken: (token: string) => void
   ): Promise<void> {
     // ... existing setup ...
     while (!result.done) {
       const event = result.value;
       if (event.token) {
-        onToken(event.token);
+        onToken(this.stripEosTokens(event.token));
       }
       // ...
     }
   }
🤖 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/services/VLMService.ts` around lines
98 - 100, The stream handler in processImage is sending raw tokens to onToken
including EOS markers; add a helper (e.g., stripEOSToken) that removes common
EOS markers ("<|end|>", "<|endoftext|>", "</s>", etc.) or returns an empty
string when a token is only an EOS, then call that helper inside the block where
processImage currently does onToken(event.token) and only invoke onToken with
the sanitized token if it is non-empty. Ensure the helper is referenced from
processImage so all streamed tokens are sanitized before being forwarded to
onToken.

Source: Coding guidelines

🧹 Nitpick comments (1)
examples/flutter/RunAnywhereAI/lib/features/more/more_view.dart (1)

66-72: ⚡ Quick win

Consider error handling for navigation failures.

The _push helper uses unawaited(Navigator.push(...)) without catching potential navigation errors. If the route fails to push (e.g., widget build errors, context issues), the failure would be silently ignored.

🛡️ Suggested improvement
 void _push(BuildContext context, Widget view) {
-  unawaited(
-    Navigator.of(
-      context,
-    ).push<void>(MaterialPageRoute<void>(builder: (_) => view)),
-  );
+  unawaited(
+    Navigator.of(context)
+        .push<void>(MaterialPageRoute<void>(builder: (_) => view))
+        .catchError((error) {
+      debugPrint('[MoreView] Navigation failed: $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/flutter/RunAnywhereAI/lib/features/more/more_view.dart` around lines
66 - 72, The _push helper currently calls
unawaited(Navigator.of(context).push(...)) so navigation errors are silently
ignored; change it to handle failures by awaiting or attaching an error handler
to the future from Navigator.push (e.g., use await inside an async _push or call
.catchError on the Future returned by Navigator.of(context).push) and
surface/log the error (using debugPrint, Logger, or ScaffoldMessenger) so
build/context/navigation exceptions in Navigator.push are not swallowed.
🤖 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/settings/tool_settings_view_model.dart`:
- Around line 34-38: The setter toolCallingEnabled currently launches the async
_saveSettingsAndSyncTools() and then immediately calls notifyListeners(),
causing listeners to see stale _registeredTools while refreshRegisteredTools()
runs; fix by moving the notifyListeners() call out of the setter and into the
end of _saveSettingsAndSyncTools() (after refreshRegisteredTools()/tool
registration/clearing completes) so listeners are notified only after sync
finishes, or alternatively keep the immediate notifyListeners() but add a clear
comment in the toolCallingEnabled setter documenting that _registeredTools may
lag until refreshRegisteredTools() completes.

In `@examples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.ts`:
- Line 154: The hook useVLMCamera was previously stripping EOS/special tokens
but that logic was removed and not moved into VLMService, so end-of-sequence
artifacts now reach the UI; add EOS-stripping inside VLMService.processImage by
implementing a private helper (e.g., stripEosTokens) that removes common EOS
tokens like <|end|>, <|endoftext|>, </s>, <eos> and call that helper on every
token before invoking the onToken callback (so callers like the
setCurrentDescription update in useVLMCamera receive cleaned tokens).

In `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx`:
- Around line 212-215: Remove the console.log diagnostic in ChatScreen.tsx and
replace it with an allowed logging level (e.g., console.warn) or remove the
statement entirely; specifically update the logging call that prints generation
options (the message using temperature, maxTokens, systemPrompt,
thinkingModeEnabled in the getGenerationOptions context) so it uses console.warn
(or console.error if appropriate) instead of console.log and drop the
eslint-disable comment.

In `@examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx`:
- Around line 20-26: The ITEMS array entry for the MoreItem with route 'STT'
uses the inappropriate icon 'pulse-outline'; update that object's icon field to
a microphone icon (e.g., 'mic-outline') so it semantically matches
Transcribe/Speech-to-text and avoids confusion with the VAD item which uses
'mic-circle-outline'; locate the ITEMS constant and change the icon value for
the object where route === 'STT'.

---

Outside diff comments:
In `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx`:
- Around line 377-383: Remove the console.log call in ChatScreen.tsx that prints
"[ChatScreen] Starting streaming generation for:" (which references prompt and
currentModel?.id); either delete the entire logging statement and the
accompanying eslint-disable-next-line comment, or replace it with console.warn
or console.error per guidelines (e.g., console.warn('[ChatScreen] Starting
streaming generation for:', prompt, 'model:', currentModel?.id)); update the
surrounding code in the ChatScreen component/streaming generation handler
accordingly.

In `@examples/react-native/RunAnywhereAI/src/services/VLMService.ts`:
- Around line 98-100: The stream handler in processImage is sending raw tokens
to onToken including EOS markers; add a helper (e.g., stripEOSToken) that
removes common EOS markers ("<|end|>", "<|endoftext|>", "</s>", etc.) or returns
an empty string when a token is only an EOS, then call that helper inside the
block where processImage currently does onToken(event.token) and only invoke
onToken with the sanitized token if it is non-empty. Ensure the helper is
referenced from processImage so all streamed tokens are sanitized before being
forwarded to onToken.

---

Nitpick comments:
In `@examples/flutter/RunAnywhereAI/lib/features/more/more_view.dart`:
- Around line 66-72: The _push helper currently calls
unawaited(Navigator.of(context).push(...)) so navigation errors are silently
ignored; change it to handle failures by awaiting or attaching an error handler
to the future from Navigator.push (e.g., use await inside an async _push or call
.catchError on the Future returned by Navigator.of(context).push) and
surface/log the error (using debugPrint, Logger, or ScaffoldMessenger) so
build/context/navigation exceptions in Navigator.push are not swallowed.
🪄 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: 238f9db8-6743-45e4-9345-f7a00f805e4d

📥 Commits

Reviewing files that changed from the base of the PR and between 1e115b0 and e421292.

📒 Files selected for processing (41)
  • examples/flutter/RunAnywhereAI/lib/app/content_view.dart
  • examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_status_components.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_types.dart
  • examples/flutter/RunAnywhereAI/lib/features/more/more_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart
  • examples/react-native/RunAnywhereAI/App.tsx
  • examples/react-native/RunAnywhereAI/README.md
  • examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx
  • examples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsx
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.ts
  • examples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx
  • examples/react-native/RunAnywhereAI/src/services/VLMService.ts
  • examples/react-native/RunAnywhereAI/src/types/index.ts
  • examples/react-native/RunAnywhereAI/src/types/settings.ts
  • examples/react-native/RunAnywhereAI/src/utils/loraFixture.ts
  • examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts
  • 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/Public/Extensions/LLM/RunAnywhere+TextGeneration.ts
  • thoughts/shared/plans/rn_flutter_strict_swift_parity.md
💤 Files with no reviewable changes (4)
  • examples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsx
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
  • examples/react-native/RunAnywhereAI/src/utils/loraFixture.ts
✅ Files skipped from review due to trivial changes (1)
  • examples/react-native/RunAnywhereAI/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx
👮 Files not reviewed due to content moderation or server errors (8)
  • examples/react-native/RunAnywhereAI/src/types/settings.ts
  • examples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dart
  • examples/react-native/RunAnywhereAI/App.tsx
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart
  • examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx
  • examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart
  • examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart

Comment on lines 34 to 38
set toolCallingEnabled(bool value) {
_toolCallingEnabled = value;
unawaited(_saveSettings());
unawaited(_saveSettingsAndSyncTools());
notifyListeners();
}

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

notifyListeners() called before async tool sync completes.

The setter calls notifyListeners() (line 37) immediately after launching the async _saveSettingsAndSyncTools() operation (line 36), without awaiting it. This means listeners rebuild while tool registration/clearing is still in progress. If any listener reads _registeredTools during this window, it will see stale state until refreshRegisteredTools() completes and calls notifyListeners() again inside the async operation (lines 48, 60, 66).

Consider moving notifyListeners() inside _saveSettingsAndSyncTools() after the sync completes, or keep the immediate notification but document that _registeredTools may temporarily lag the toggle state.

🤖 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/settings/tool_settings_view_model.dart`
around lines 34 - 38, The setter toolCallingEnabled currently launches the async
_saveSettingsAndSyncTools() and then immediately calls notifyListeners(),
causing listeners to see stale _registeredTools while refreshRegisteredTools()
runs; fix by moving the notifyListeners() call out of the setter and into the
end of _saveSettingsAndSyncTools() (after refreshRegisteredTools()/tool
registration/clearing completes) so listeners are notified only after sync
finishes, or alternatively keep the immediate notifyListeners() but add a clear
comment in the toolCallingEnabled setter documenting that _registeredTools may
lag until refreshRegisteredTools() completes.

SINGLE_CAPTURE_MAX_TOKENS,
(token) => {
setCurrentDescription((prev) => stripEosTokens(prev + token));
setCurrentDescription((prev) => prev + token);

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

EOS token stripping removed but not moved to VLMService.

The hook previously stripped EOS/special tokens from VLM output; that logic was removed but not relocated. According to coding guidelines, VLMService should "Strip EOS tokens from VLM model output... before returning results." Review of VLMService.ts shows no EOS-stripping logic was added there, so end-of-sequence artifacts will now reach the UI.

💡 Suggested fix

Move the EOS token stripping into VLMService.processImage(). In VLMService.ts, add a helper to strip common EOS tokens and apply it before calling onToken:

// In VLMService.ts
private stripEosTokens(text: string): string {
  return text.replace(/<\|end\|>|<\|endoftext\|>|<\/s>|<eos>/g, '');
}

async processImage(
  imagePath: string,
  prompt: string,
  maxTokens: number,
  onToken: (token: string) => void
): Promise<void> {
  // ... existing code ...
  while (!result.done) {
    const event = result.value;
    if (event.token) {
      onToken(this.stripEosTokens(event.token));
    }
    // ...
  }
}

Also applies to: 187-187, 220-220

🤖 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/hooks/useVLMCamera.ts` at line 154,
The hook useVLMCamera was previously stripping EOS/special tokens but that logic
was removed and not moved into VLMService, so end-of-sequence artifacts now
reach the UI; add EOS-stripping inside VLMService.processImage by implementing a
private helper (e.g., stripEosTokens) that removes common EOS tokens like
<|end|>, <|endoftext|>, </s>, <eos> and call that helper on every token before
invoking the onToken callback (so callers like the setCurrentDescription update
in useVLMCamera receive cleaned tokens).

Source: Coding guidelines

Comment thread examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx
Comment on lines +20 to +26
const ITEMS: MoreItem[] = [
{
route: 'STT',
title: 'Transcribe',
subtitle: 'Speech-to-text',
icon: 'pulse-outline',
},

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

Icon mismatch for STT route.

The STT (Speech-to-Text/Transcribe) item uses 'pulse-outline', which typically represents heartbeat/pulse/activity. For speech transcription, a microphone icon like 'mic-outline' would be more semantically appropriate. Note that VAD (line 43) uses 'mic-circle-outline', creating potential visual confusion between the two audio-related features.

🎨 Suggested icon fix
   {
     route: 'STT',
     title: 'Transcribe',
     subtitle: 'Speech-to-text',
-    icon: 'pulse-outline',
+    icon: 'mic-outline',
   },
📝 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 ITEMS: MoreItem[] = [
{
route: 'STT',
title: 'Transcribe',
subtitle: 'Speech-to-text',
icon: 'pulse-outline',
},
const ITEMS: MoreItem[] = [
{
route: 'STT',
title: 'Transcribe',
subtitle: 'Speech-to-text',
icon: 'mic-outline',
},
🤖 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/MoreScreen.tsx` around lines
20 - 26, The ITEMS array entry for the MoreItem with route 'STT' uses the
inappropriate icon 'pulse-outline'; update that object's icon field to a
microphone icon (e.g., 'mic-outline') so it semantically matches
Transcribe/Speech-to-text and avoids confusion with the VAD item which uses
'mic-circle-outline'; locate the ITEMS constant and change the icon value for
the object where route === 'STT'.

sanchitmonga22 and others added 2 commits June 11, 2026 14:37
- Wire-request parity: LLM defaults (100/0.8/1.0/0/1.0), tool-calling
  llmOptions channel (topP passthrough, validateCalls unset), download
  defaults un-inverted + explicit registry import, VLM 256/0.7/0.9/40,
  RAG defaults from generated rAGQueryOptionsDefaults()
- Real generateStructuredStream (canonical StructuredOutputStreamEvent
  token stream, Swift cancel semantics) + aggregateStream +
  downloadModelStream + generate/generateStream request overloads
- Ported missing surface: voice-agent compose validation, LoRA download
  surface + non-throwing checkCompatibility, CloudSTT
  registerProvider/unregisterProvider, SDKException helpers, pcm16ToWav,
  AudioConvert wired onto the facade
- Deleted legacy: SpeechProvider routing fork (+ dead branches/helpers),
  namespace duplicates of flat Swift names (rag/voiceAgent/
  StructuredOutput/audioConvert), modelRegistry+downloads moved to
  internal.ts, SDKInitOptions.debug, SDKLogger.trace, SDKException
  componentNotReady/generationFailed/invalidInput/.protoCode/.details
- Name-level parity: SDKException.code = positive proto code (+.cAbiCode),
  error taxonomy remapped to Swift codes, EventBus emit->publish + typed
  payload accessors, flat logging delegates, SDK_NAME/SDK_PLATFORM
- clearCache/cleanTempFiles via new rac_wasm_file_manager_clear_* WASM
  shims (commons-owned semantics) + cloud STT provider exports; stale
  voice-agent sizeof helper removed
- proto-ts: regenerated, zero drift (single shared package for RN + Web)

Gates: web typecheck 0, vitest 83/83, example build 0, RN typecheck 0,
WASM rebuilt, RN iOS example BUILD SUCCEEDED.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dOutput

Closes the two Swift-surface gaps reported during the Web alignment:
- ragResolvedConfiguration loads embedding + LLM artifacts through the
  commons lifecycle (category fallback from the registry entry,
  Swift-shaped MODEL_LOAD_FAILED errors) and stamps the resolved model
  ids onto the defaults-seeded RAGConfiguration; the
  ragCreatePipeline(embeddingModelId, llmModelId) bootstrap overload now
  composes through it (Swift RunAnywhere+RAG.swift:19-50 parity — the
  overload previously skipped the lifecycle loads)
- generateWithStructuredOutput applies a structured-output configuration
  to a non-streaming generate, running the commons preparePrompt
  primitive when includeSchemaInPrompt and adopting its system prompt
  (Swift RunAnywhere+StructuredOutput.swift:139-156)

Both exposed flat on the RunAnywhere facade like Swift.
Gates: web typecheck 0, vitest 83/83, core + example builds 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Caution

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

⚠️ Outside diff range comments (5)
examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx (1)

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

Tool calling state may not reflect actual registration status on failure.

Lines 349-350 set toolCallingEnabled to true and persist it to AsyncStorage immediately, before the async registerSharedDemoTools() completes. If registration fails (e.g., network error, SDK error), the UI will show tool calling as enabled but no tools will be registered.

Consider moving setToolCallingEnabled and AsyncStorage.setItem inside the try block, after successful registration, or reverting the state on failure.

🔧 Suggested fix
 const handleToggleToolCalling = async (enabled: boolean) => {
-  setToolCallingEnabled(enabled);
   try {
-    await AsyncStorage.setItem(
-      STORAGE_KEYS.TOOL_CALLING_ENABLED,
-      enabled ? 'true' : 'false'
-    );
     if (enabled) {
       await registerSharedDemoTools();
     } else {
       await RunAnywhere.clearTools();
     }
     await refreshRegisteredTools();
+    setToolCallingEnabled(enabled);
+    await AsyncStorage.setItem(
+      STORAGE_KEYS.TOOL_CALLING_ENABLED,
+      enabled ? 'true' : 'false'
+    );
   } catch (error) {
     console.error('[Settings] Failed to save tool calling setting:', error);
+    // Revert UI state on failure
+    setToolCallingEnabled(!enabled);
   }
 };
🤖 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 348 - 363, The handler handleToggleToolCalling currently sets UI state and
writes STORAGE_KEYS.TOOL_CALLING_ENABLED before attempting async registration,
causing the UI to show enabled even if registerSharedDemoTools() fails; change
it so the state update and AsyncStorage.setItem are performed only after
successful registerSharedDemoTools() (or after RunAnywhere.clearTools() on
disable), or keep the optimistic update but revert it and update AsyncStorage in
the catch block; specifically update handleToggleToolCalling to call
registerSharedDemoTools() / RunAnywhere.clearTools() and
refreshRegisteredTools() first, then call setToolCallingEnabled(enabled) and
AsyncStorage.setItem(...), and in the catch block ensure you revert
setToolCallingEnabled(false) (or to the previous value) and persist that so UI
and storage reflect actual registration status.
examples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dart (1)

138-191: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add timeouts around the Open-Meteo calls.

Both http.get() calls can hang indefinitely. When this tool is invoked from generateWithTools(), a stalled network hop keeps the whole response path open until the user cancels.

Suggested fix
-      final geocodeResponse = await http.get(geocodeUrl);
+      final geocodeResponse = await http
+          .get(geocodeUrl)
+          .timeout(const Duration(seconds: 10));
@@
-      final weatherResponse = await http.get(weatherUrl);
+      final weatherResponse = await http
+          .get(weatherUrl)
+          .timeout(const Duration(seconds: 10));
🤖 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/settings/tool_settings_view_model.dart`
around lines 138 - 191, The geocoding and weather HTTP requests (the two
http.get calls that produce geocodeResponse and weatherResponse in
tool_settings_view_model.dart) can hang indefinitely; wrap each http.get(...)
with a timeout (e.g., .timeout(const Duration(seconds: 5))) and catch
TimeoutException to return a clear error result. Add the timeout to both the
geocodeUrl request and the weatherUrl request, and update the catch block (or
add a specific on TimeoutException) to return {'error': 'Weather fetch timed
out', 'location': location} (or similar) so _weatherCodeToCondition and the rest
of the method handle a timely failure.
examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx (1)

315-329: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Selection never enters a loading state.

isLoadingModel and selectedModelId are only consumed by the UI; they are never set before awaiting onModelSelected(model). That leaves the Select button hot during a heavyweight load and allows duplicate load requests for the same model.

Suggested fix
   const handleSelectModel = async (model: SDKModelInfo) => {
     console.warn('[ModelSelectionSheet] Select tapped:', model.id);
     if (!model.isDownloaded && !model.localPath) {
       return;
     }
 
     try {
+      setIsLoadingModel(true);
+      setSelectedModelId(model.id);
       if (isRAGContext(context)) {
         // RAG models are referenced by file path at pipeline creation time,
         // not pre-loaded into memory. Just pass the selection back and close.
         await onModelSelected(model);
         onClose();
       } else {
         await onModelSelected(model);
       }
     } catch (error) {
       console.error('[ModelSelectionSheet] Error selecting model:', error);
+    } finally {
+      setIsLoadingModel(false);
+      setSelectedModelId(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/src/components/model/ModelSelectionSheet.tsx`
around lines 315 - 329, handleSelectModel never sets selectedModelId or
isLoadingModel before awaiting onModelSelected, so the UI never shows loading
and duplicate clicks are allowed; update handleSelectModel to set
selectedModelId = model.id and isLoadingModel = true immediately after the
early-return check (and no-op if selectedModelId === model.id and isLoadingModel
=== true), then await onModelSelected(model) (handling both RAG and non-RAG
branches), and in a finally block reset isLoadingModel = false and clear
selectedModelId if appropriate; reference the handleSelectModel function,
isLoadingModel, selectedModelId, onModelSelected, onClose, and isRAGContext when
making the change.
examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart (2)

456-462: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't clear the transcript while a fixed-index placeholder is still streaming.

_generateStreaming() and _generateWithToolCalling() keep writing back into _messages[messageIndex]. If _clearChat() runs mid-generation, those updates target a removed row and can throw or land on the wrong message.

🤖 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/chat/chat_interface_view.dart`
around lines 456 - 462, The bug is that _clearChat() unconditionally clears
_messages while asynchronous generators (_generateStreaming() and
_generateWithToolCalling()) are still writing to _messages[messageIndex],
causing races or out-of-range writes; fix by preventing or cancelling active
generation before clearing: introduce or use an existing cancellation flag/token
(e.g. _isStreaming or a _generationCancelToken) that _generateStreaming() and
_generateWithToolCalling() respect, update those functions to abort early when
cancelled, and modify _clearChat() to set the cancel flag (or call a
_cancelGeneration() method) and await termination of the generator before
calling setState to clear
_messages/_errorMessage/_currentStreamingContent/_currentThinkingContent so no
writes race with the clear. Ensure references to _messages and messageIndex are
only mutated after cancellation completes.

180-193: ⚠️ Potential issue | 🟠 Major

Don’t allow _clearChat() to run during _generateStreaming() token updates (and tool-format selection is already correct)

  • chat_interface_view.dart’s _generateStreaming() captures messageIndex and updates _messages[messageIndex] inside the streaming onToken callback with no bounds guard.
  • _clearChat() (delete button and “new chat” from conversation history) clears _messages via _messages.clear() while generation may still be in progress, so the captured messageIndex can become invalid, causing out-of-range updates / writing into the wrong placeholder row.
  • The LFM2 tool-call selection in _detectToolCallFormat() returning ToolCallFormatName.TOOL_CALL_FORMAT_NAME_PYTHONIC for lfm2+tool matches the SDK/commons mapping: TOOL_CALL_FORMAT_NAME_PYTHONIC resolves to the "lfm2" hint (rac_tool_call_format_hint_from_format_name"lfm2" via resolvedFormatName).
🤖 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/chat/chat_interface_view.dart`
around lines 180 - 193, The streaming generator captures a messageIndex and
updates _messages[messageIndex] inside the onToken callback of
_generateStreaming, but _clearChat can call _messages.clear() concurrently which
makes that index invalid; update _generateStreaming to guard against
out-of-bounds/mutated message list by (1) storing a local reference or id for
the placeholder message and, before each onToken update, verifying the
messageIndex is still within range and that the message at that index matches
the expected placeholder (or use a unique message id match), and (2) support
cancellation by setting and checking a generation-in-progress flag or
cancellation token that _clearChat will set/trigger to stop updates; do not
change _detectToolCallFormat (its LFM2/tool behavior is correct).

Source: Coding guidelines

♻️ Duplicate comments (3)
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx (1)

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

Remove console.log per coding guidelines.

This was flagged in a previous review. The coding guidelines specify that only console.warn and console.error are allowed in React Native code. Replace with console.warn or remove the statement.

Proposed fix
-    // eslint-disable-next-line no-console -- demo settings diagnostic
-    console.log(
-      `[PARAMS] App getGenerationOptions: temperature=${temperature}, maxTokens=${maxTokens}, systemPrompt=${systemPrompt ? `set(${systemPrompt.length} chars)` : 'nil'}, thinkingMode=${thinkingModeEnabled}`
-    );
+    // Diagnostic logging changed to console.warn per coding guidelines
+    console.warn(
+      `[PARAMS] App getGenerationOptions: temperature=${temperature}, maxTokens=${maxTokens}, systemPrompt=${systemPrompt ? `set(${systemPrompt.length} chars)` : 'nil'}, thinkingMode=${thinkingModeEnabled}`
+    );
🤖 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
212 - 215, Replace the forbidden console.log in getGenerationOptions with an
allowed logging method or remove it: locate the console.log call inside the
getGenerationOptions function (the line logging temperature, maxTokens,
systemPrompt and thinkingModeEnabled) and either change console.log(...) to
console.warn(...) or delete the statement entirely to comply with React Native
coding guidelines that only permit console.warn and console.error.

Source: Coding guidelines

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

54-54: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This bypasses the required platform-specific STT capture paths.

STTScreen now hard-wires AudioCaptureManager for both platforms. According to coding guidelines, this sample is supposed to use the iOS AVFoundation path via NativeAudioModule.startRecording() and the Android native recorder path with RunAnywhere.transcribe().

As per coding guidelines: examples/react-native/RunAnywhereAI/**/{STTScreen,VoiceAssistantScreen}.tsx: "Import platform-specific STT recording: use NativeAudioModule.startRecording() (AVFoundation) on iOS and native audio recorder with RunAnywhere.transcribe() on Android".

Also applies to: 467-481

🤖 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` at line 54,
STTScreen currently hard-codes importing and using AudioCaptureManager which
bypasses platform-specific STT capture; instead remove the direct use/import of
AudioCaptureManager in STTScreen and implement platform branching so iOS uses
NativeAudioModule.startRecording() (AVFoundation path) and Android uses the
native recorder via RunAnywhere.transcribe() (or the Android native recorder
wrapper). Update imports to include NativeAudioModule and RunAnywhere, replace
calls that start/stop recording in STTScreen (and the duplicated block around
467–481) to call NativeAudioModule.startRecording()/stopRecording() on iOS and
RunAnywhere.transcribe()/native recorder calls on Android, preserving existing
start/stop UI handlers and error handling.

Source: Coding guidelines

examples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.ts (1)

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

EOS token stripping removed but not moved to VLMService.

The hook previously stripped EOS/special tokens from VLM output; that logic was removed but not relocated. According to coding guidelines, VLMService should "Strip EOS tokens from VLM model output... before returning results." Review of VLMService.ts (lines 98-100 in the next file) shows no EOS-stripping logic was added there, so end-of-sequence artifacts will now reach the UI.

💡 Suggested fix

Move the EOS token stripping into VLMService.processImage(). In VLMService.ts, add a helper to strip common EOS tokens and apply it before calling onToken:

// In VLMService.ts
private stripEosTokens(text: string): string {
  return text.replace(/<\|end\|>|<\|endoftext\|>|<\/s>|<eos>/g, '');
}

async processImage(
  imagePath: string,
  prompt: string,
  maxTokens: number,
  onToken: (token: string) => void
): Promise<void> {
  // ... existing code ...
  while (!result.done) {
    const event = result.value;
    if (event.token) {
      onToken(this.stripEosTokens(event.token));
    }
    // ...
  }
}

Also applies to: 187-187, 220-220

🤖 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/hooks/useVLMCamera.ts` at line 154,
The hook appending tokens via setCurrentDescription now lets EOS/special tokens
reach the UI because EOS-stripping was removed; move that logic into VLMService
by adding a helper (e.g., stripEosTokens) on the VLMService class and call it
inside VLMService.processImage before invoking onToken (i.e., when handling
streaming events with event.token), so all callers (including the hook which
uses setCurrentDescription) receive cleaned tokens; ensure the regex removes
common EOS variants like <|end|>, <|endoftext|>, </s>, <eos> and apply it
wherever processImage emits tokens.

Source: Coding guidelines

🧹 Nitpick comments (2)
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx (1)

200-217: Thinking mode currently defaults to false (key-missing path)

Both ChatScreen and SettingsScreen derive thinkingModeEnabled as thinkingStr === 'true' (so when THINKING_MODE_ENABLED is missing/null, it resolves to false), and SettingsScreen initializes the toggle state with useState(false). If thinking is meant to be enabled-by-default (opt-out), adjust the null/default handling in both places.

🤖 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
200 - 217, The thinking-mode defaults to false when the storage key is missing;
change the null/default handling so missing key enables thinking-mode by
default: in the fetch path that computes thinkingModeEnabled (look for
GENERATION_SETTINGS_KEYS.THINKING_MODE_ENABLED and the variable
thinkingModeEnabled in ChatScreen/getGenerationOptions) treat null/undefined as
'true' (e.g., thinkingModeEnabled = thinkingStr === null ? true : thinkingStr
=== 'true'), and update SettingsScreen's toggle initial state (search for
useState(false) that controls thinkingMode) to initialize to true when no
persisted value exists so the UI and storage-default behavior match.
examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx (1)

113-175: 💤 Low value

Consider using a sentinel value or proper typing for done state.

The manual async iterator implementation uses undefined as unknown as Uint8Array in multiple places (lines 137, 152, 161) to satisfy TypeScript when returning done results. While functional, this type assertion bypasses type safety.

♻️ Alternative approach

Consider using a proper union type or adjusting the iterator signature:

type PushableAudioStream = {
  iterable: AsyncIterable<Uint8Array>;
  push: (chunk: Uint8Array) => void;
  close: () => void;
};

function createPushableAudioStream(): PushableAudioStream {
  const queue: Uint8Array[] = [];
  const waiters: Array<(result: IteratorResult<Uint8Array, void>) => void> = [];
  let closed = false;

  // ... existing logic ...

  return {
    iterable: {
      [Symbol.asyncIterator](): AsyncIterator<Uint8Array, void> {
        return {
          async next(): Promise<IteratorResult<Uint8Array, void>> {
            const chunk = queue.shift();
            if (chunk) return { value: chunk, done: false };
            if (closed) return { value: undefined, done: true };
            return new Promise((resolve) => waiters.push(resolve));
          },
          async return(): Promise<IteratorResult<Uint8Array, void>> {
            finish();
            return { value: undefined, done: true };
          },
        };
      },
    },
    // ... rest
  };
}
🤖 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
113 - 175, The iterator is using unsafe casts like `undefined as unknown as
Uint8Array`; update PushableAudioStream/createPushableAudioStream so the
iterator return type properly models the "done" case (e.g., use
AsyncIterator<Uint8Array, void> or IteratorResult<Uint8Array, void>) instead of
forcing undefined into Uint8Array: change the waiters type to Array<(result:
IteratorResult<Uint8Array, void>) => void>, adjust the iterable's
[Symbol.asyncIterator]() to return AsyncIterator<Uint8Array, void>, and update
next() and return() to return IteratorResult<Uint8Array, void> with value:
undefined for done cases and remove the unsafe casts.
🤖 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/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsx`:
- Around line 46-47: The VAD icon is inconsistent: ModelRequiredOverlay's case
'vad' returns 'pulse-outline' while MoreScreen's VAD navigation item uses
'mic-circle-outline'; update the VAD icon in MoreScreen.tsx (the VAD navigation
item/icon prop) to use 'pulse-outline' so both ModelRequiredOverlay (case 'vad')
and the MoreScreen VAD entry use the same 'pulse-outline' icon for consistency.

In `@examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx`:
- Around line 187-209: The startListening flow sets taskRef.current and
isListeningRef.current before capture.startRecording succeeds, so if
startRecording throws the VAD task is orphaned; modify startListening to only
assign taskRef.current, isListeningRef.current, streamRef.current and
setIsListening(true) after await capture.startRecording completes successfully,
or wrap startRecording in try/catch and on error perform rollback: stop/end the
pushable stream created by createPushableAudioStream, clear streamRef.current,
cancel/clear taskRef.current (the consumeVAD(RunAnywhere.streamVAD(...)) task),
set isListeningRef.current = false, and leave UI state (setIsListening,
setFrameCount, setLatestResult) consistent; reference functions/vars:
startListening, createPushableAudioStream, pcm16ChunkToFloat32Bytes,
capture.startRecording, consumeVAD, RunAnywhere.streamVAD, taskRef, streamRef,
isListeningRef.

In `@examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts`:
- Around line 9-10: The DEFAULT_INFERENCE_FRAMEWORK constant was changed to
InferenceFramework.INFERENCE_FRAMEWORK_UNSPECIFIED which can make SDK inference
calls fail when no framework is selected; either revert
DEFAULT_INFERENCE_FRAMEWORK back to
InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP or update downstream handling
in ChatScreen.tsx where the framework is read (the code around the framework
selection/SDK call) to detect INFERENCE_FRAMEWORK_UNSPECIFIED and choose a
compatible fallback programmatically (or surface a clear error/selection UI)
before making the SDK call so no UNSPECIFIED value is passed to the SDK.

---

Outside diff comments:
In `@examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart`:
- Around line 456-462: The bug is that _clearChat() unconditionally clears
_messages while asynchronous generators (_generateStreaming() and
_generateWithToolCalling()) are still writing to _messages[messageIndex],
causing races or out-of-range writes; fix by preventing or cancelling active
generation before clearing: introduce or use an existing cancellation flag/token
(e.g. _isStreaming or a _generationCancelToken) that _generateStreaming() and
_generateWithToolCalling() respect, update those functions to abort early when
cancelled, and modify _clearChat() to set the cancel flag (or call a
_cancelGeneration() method) and await termination of the generator before
calling setState to clear
_messages/_errorMessage/_currentStreamingContent/_currentThinkingContent so no
writes race with the clear. Ensure references to _messages and messageIndex are
only mutated after cancellation completes.
- Around line 180-193: The streaming generator captures a messageIndex and
updates _messages[messageIndex] inside the onToken callback of
_generateStreaming, but _clearChat can call _messages.clear() concurrently which
makes that index invalid; update _generateStreaming to guard against
out-of-bounds/mutated message list by (1) storing a local reference or id for
the placeholder message and, before each onToken update, verifying the
messageIndex is still within range and that the message at that index matches
the expected placeholder (or use a unique message id match), and (2) support
cancellation by setting and checking a generation-in-progress flag or
cancellation token that _clearChat will set/trigger to stop updates; do not
change _detectToolCallFormat (its LFM2/tool behavior is correct).

In
`@examples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dart`:
- Around line 138-191: The geocoding and weather HTTP requests (the two http.get
calls that produce geocodeResponse and weatherResponse in
tool_settings_view_model.dart) can hang indefinitely; wrap each http.get(...)
with a timeout (e.g., .timeout(const Duration(seconds: 5))) and catch
TimeoutException to return a clear error result. Add the timeout to both the
geocodeUrl request and the weatherUrl request, and update the catch block (or
add a specific on TimeoutException) to return {'error': 'Weather fetch timed
out', 'location': location} (or similar) so _weatherCodeToCondition and the rest
of the method handle a timely failure.

In
`@examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx`:
- Around line 315-329: handleSelectModel never sets selectedModelId or
isLoadingModel before awaiting onModelSelected, so the UI never shows loading
and duplicate clicks are allowed; update handleSelectModel to set
selectedModelId = model.id and isLoadingModel = true immediately after the
early-return check (and no-op if selectedModelId === model.id and isLoadingModel
=== true), then await onModelSelected(model) (handling both RAG and non-RAG
branches), and in a finally block reset isLoadingModel = false and clear
selectedModelId if appropriate; reference the handleSelectModel function,
isLoadingModel, selectedModelId, onModelSelected, onClose, and isRAGContext when
making the change.

In `@examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx`:
- Around line 348-363: The handler handleToggleToolCalling currently sets UI
state and writes STORAGE_KEYS.TOOL_CALLING_ENABLED before attempting async
registration, causing the UI to show enabled even if registerSharedDemoTools()
fails; change it so the state update and AsyncStorage.setItem are performed only
after successful registerSharedDemoTools() (or after RunAnywhere.clearTools() on
disable), or keep the optimistic update but revert it and update AsyncStorage in
the catch block; specifically update handleToggleToolCalling to call
registerSharedDemoTools() / RunAnywhere.clearTools() and
refreshRegisteredTools() first, then call setToolCallingEnabled(enabled) and
AsyncStorage.setItem(...), and in the catch block ensure you revert
setToolCallingEnabled(false) (or to the previous value) and persist that so UI
and storage reflect actual registration status.

---

Duplicate comments:
In `@examples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.ts`:
- Line 154: The hook appending tokens via setCurrentDescription now lets
EOS/special tokens reach the UI because EOS-stripping was removed; move that
logic into VLMService by adding a helper (e.g., stripEosTokens) on the
VLMService class and call it inside VLMService.processImage before invoking
onToken (i.e., when handling streaming events with event.token), so all callers
(including the hook which uses setCurrentDescription) receive cleaned tokens;
ensure the regex removes common EOS variants like <|end|>, <|endoftext|>, </s>,
<eos> and apply it wherever processImage emits tokens.

In `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx`:
- Around line 212-215: Replace the forbidden console.log in getGenerationOptions
with an allowed logging method or remove it: locate the console.log call inside
the getGenerationOptions function (the line logging temperature, maxTokens,
systemPrompt and thinkingModeEnabled) and either change console.log(...) to
console.warn(...) or delete the statement entirely to comply with React Native
coding guidelines that only permit console.warn and console.error.

In `@examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx`:
- Line 54: STTScreen currently hard-codes importing and using
AudioCaptureManager which bypasses platform-specific STT capture; instead remove
the direct use/import of AudioCaptureManager in STTScreen and implement platform
branching so iOS uses NativeAudioModule.startRecording() (AVFoundation path) and
Android uses the native recorder via RunAnywhere.transcribe() (or the Android
native recorder wrapper). Update imports to include NativeAudioModule and
RunAnywhere, replace calls that start/stop recording in STTScreen (and the
duplicated block around 467–481) to call
NativeAudioModule.startRecording()/stopRecording() on iOS and
RunAnywhere.transcribe()/native recorder calls on Android, preserving existing
start/stop UI handlers and error handling.

---

Nitpick comments:
In `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx`:
- Around line 200-217: The thinking-mode defaults to false when the storage key
is missing; change the null/default handling so missing key enables
thinking-mode by default: in the fetch path that computes thinkingModeEnabled
(look for GENERATION_SETTINGS_KEYS.THINKING_MODE_ENABLED and the variable
thinkingModeEnabled in ChatScreen/getGenerationOptions) treat null/undefined as
'true' (e.g., thinkingModeEnabled = thinkingStr === null ? true : thinkingStr
=== 'true'), and update SettingsScreen's toggle initial state (search for
useState(false) that controls thinkingMode) to initialize to true when no
persisted value exists so the UI and storage-default behavior match.

In `@examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx`:
- Around line 113-175: The iterator is using unsafe casts like `undefined as
unknown as Uint8Array`; update PushableAudioStream/createPushableAudioStream so
the iterator return type properly models the "done" case (e.g., use
AsyncIterator<Uint8Array, void> or IteratorResult<Uint8Array, void>) instead of
forcing undefined into Uint8Array: change the waiters type to Array<(result:
IteratorResult<Uint8Array, void>) => void>, adjust the iterable's
[Symbol.asyncIterator]() to return AsyncIterator<Uint8Array, void>, and update
next() and return() to return IteratorResult<Uint8Array, void> with value:
undefined for done cases and remove the unsafe casts.
🪄 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: 1d2f968e-1b86-4596-ad9e-387ac230f663

📥 Commits

Reviewing files that changed from the base of the PR and between 1e115b0 and 19282b1.

📒 Files selected for processing (49)
  • examples/flutter/RunAnywhereAI/lib/app/content_view.dart
  • examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_status_components.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_types.dart
  • examples/flutter/RunAnywhereAI/lib/features/more/more_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart
  • examples/react-native/RunAnywhereAI/App.tsx
  • examples/react-native/RunAnywhereAI/README.md
  • examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx
  • examples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsx
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.ts
  • examples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx
  • examples/react-native/RunAnywhereAI/src/services/VLMService.ts
  • examples/react-native/RunAnywhereAI/src/types/index.ts
  • examples/react-native/RunAnywhereAI/src/types/settings.ts
  • examples/react-native/RunAnywhereAI/src/utils/loraFixture.ts
  • examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts
  • examples/web/RunAnywhereAI/src/components/model-selection.ts
  • examples/web/RunAnywhereAI/src/main.ts
  • examples/web/RunAnywhereAI/src/services/model-catalog.ts
  • examples/web/RunAnywhereAI/src/views/solutions.ts
  • examples/web/RunAnywhereAI/src/views/vision.ts
  • 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/Public/Extensions/LLM/RunAnywhere+TextGeneration.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/EventBus.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/RACErrors.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/SDKException.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts
💤 Files with no reviewable changes (5)
  • examples/react-native/RunAnywhereAI/src/utils/loraFixture.ts
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
  • examples/web/RunAnywhereAI/src/services/model-catalog.ts
  • examples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsx
✅ Files skipped from review due to trivial changes (2)
  • examples/web/RunAnywhereAI/src/components/model-selection.ts
  • examples/react-native/RunAnywhereAI/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx

Comment on lines +46 to +47
case 'vad':
return 'pulse-outline';

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

VAD icon inconsistency across components.

The VAD modality uses 'pulse-outline' here, but in MoreScreen.tsx line 43, the VAD navigation item uses 'mic-circle-outline'. For better UX consistency, the same feature should use the same icon throughout the app. The 'pulse-outline' icon (used here) seems more semantically appropriate for voice activity detection since it represents waveforms/activity rather than microphone input.

🎨 Suggested fix for MoreScreen.tsx

In examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx:

   {
     route: 'VAD',
     title: 'Voice Activity',
     subtitle: 'Speech detection stream',
-    icon: 'mic-circle-outline',
+    icon: 'pulse-outline',
   },
🤖 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/common/ModelRequiredOverlay.tsx`
around lines 46 - 47, The VAD icon is inconsistent: ModelRequiredOverlay's case
'vad' returns 'pulse-outline' while MoreScreen's VAD navigation item uses
'mic-circle-outline'; update the VAD icon in MoreScreen.tsx (the VAD navigation
item/icon prop) to use 'pulse-outline' so both ModelRequiredOverlay (case 'vad')
and the MoreScreen VAD entry use the same 'pulse-outline' icon for consistency.

Comment on lines +187 to +209
const startListening = async () => {
if (!currentModel) {
Alert.alert('Model Required', 'Please select a VAD model first.');
return;
}
const capture = getCapture();
const granted = await capture.requestPermission();
if (!granted) {
Alert.alert('Microphone Required', 'Microphone permission is required.');
return;
}

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);

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 | 🟠 Major | ⚡ Quick win

Roll back VAD startup when recording init fails.

taskRef.current and isListeningRef.current are set before startRecording() succeeds, and this path has no rollback. If microphone startup throws, the orphaned streamVAD task stays alive and the next tap can start a second session on top of it.

Suggested fix
   const startListening = async () => {
+    if (isListeningRef.current || isListening) {
+      return;
+    }
     if (!currentModel) {
       Alert.alert('Model Required', 'Please select a VAD model first.');
       return;
@@
     const stream = createPushableAudioStream();
     streamRef.current = stream;
     isListeningRef.current = true;
+    setIsListening(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));
+      });
+    } catch (error) {
+      isListeningRef.current = false;
+      stream.close();
+      streamRef.current = null;
+      taskRef.current = null;
+      setIsListening(false);
+      Alert.alert('VAD Failed', String(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
187 - 209, The startListening flow sets taskRef.current and
isListeningRef.current before capture.startRecording succeeds, so if
startRecording throws the VAD task is orphaned; modify startListening to only
assign taskRef.current, isListeningRef.current, streamRef.current and
setIsListening(true) after await capture.startRecording completes successfully,
or wrap startRecording in try/catch and on error perform rollback: stop/end the
pushable stream created by createPushableAudioStream, clear streamRef.current,
cancel/clear taskRef.current (the consumeVAD(RunAnywhere.streamVAD(...)) task),
set isListeningRef.current = false, and leave UI state (setIsListening,
setFrameCount, setLatestResult) consistent; reference functions/vars:
startListening, createPushableAudioStream, pcm16ChunkToFloat32Bytes,
capture.startRecording, consumeVAD, RunAnywhere.streamVAD, taskRef, streamRef,
isListeningRef.

Comment on lines 9 to +10
export const DEFAULT_INFERENCE_FRAMEWORK =
InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP;
export const SYSTEM_TTS_FRAMEWORK =
InferenceFramework.INFERENCE_FRAMEWORK_SYSTEM_TTS;
InferenceFramework.INFERENCE_FRAMEWORK_UNSPECIFIED;

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

Framework fallback changed to UNSPECIFIED.

This changes the default fallback from INFERENCE_FRAMEWORK_LLAMA_CPP to INFERENCE_FRAMEWORK_UNSPECIFIED. Verify that downstream usage (e.g., ChatScreen.tsx line 311) handles this correctly, as UNSPECIFIED may cause SDK calls to fail if no compatible framework is found. See related comment in ChatScreen.tsx.

🤖 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/utils/modelDisplay.ts` around lines 9
- 10, The DEFAULT_INFERENCE_FRAMEWORK constant was changed to
InferenceFramework.INFERENCE_FRAMEWORK_UNSPECIFIED which can make SDK inference
calls fail when no framework is selected; either revert
DEFAULT_INFERENCE_FRAMEWORK back to
InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP or update downstream handling
in ChatScreen.tsx where the framework is read (the code around the framework
selection/SDK call) to detect INFERENCE_FRAMEWORK_UNSPECIFIED and choose a
compatible fallback programmatically (or surface a clear error/selection UI)
before making the SDK call so no UNSPECIFIED value is passed to the SDK.

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

Caution

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

⚠️ Outside diff range comments (1)
examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart (1)

223-223: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Don't keep async assistant updates keyed to a stale list index.

Both the tool-calling and streaming paths capture messageIndex once, but _clearChat() can empty _messages while generation is still running. The next token/final update then writes through _messages[messageIndex] and throws a reachable RangeError.

Safer pattern
     final assistantMessage = ChatMessage(
       id: DateTime.now().millisecondsSinceEpoch.toString(),
@@
     setState(() {
       _messages.add(assistantMessage);
     });
-
-    final messageIndex = _messages.length - 1;
+    final assistantMessageId = assistantMessage.id;
@@
           if (!mounted) return;
           setState(() {
-            _messages[messageIndex] = _messages[messageIndex].copyWith(
+            final idx = _messages.indexWhere(
+              (message) => message.id == assistantMessageId,
+            );
+            if (idx == -1) {
+              return;
+            }
+            _messages[idx] = _messages[idx].copyWith(
               content: _currentStreamingContent,
             );
           });
@@
       if (!mounted) return;
       setState(() {
-        _messages[messageIndex] = _messages[messageIndex].copyWith(
+        final idx = _messages.indexWhere(
+          (message) => message.id == assistantMessageId,
+        );
+        if (idx == -1) {
+          _isGenerating = false;
+          return;
+        }
+        _messages[idx] = _messages[idx].copyWith(
           content: result.text,
           thinkingContent: _currentThinkingContent.isNotEmpty
               ? _currentThinkingContent
               : null,
           analytics: analytics,
         );
         _isGenerating = false;
       });

Apply the same lookup-by-id guard in the tool-calling completion path, or disable clearing while _isGenerating.

Also applies to: 281-286, 320-379

🤖 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/chat/chat_interface_view.dart` at
line 223, The current assistant update logic captures a stale messageIndex and
can write into _messages[messageIndex] after _clearChat() empties the list;
change to capture the message's unique id (e.g., message.id) when creating the
assistant placeholder and, in all streaming and tool-calling update paths (the
code that currently uses messageIndex), lookup the message by id each time
(findIndex or firstWhere on _messages) and abort the update if not found;
alternatively, ensure _clearChat() is disabled while _isGenerating but preferred
fix is lookup-by-id and guard early to avoid RangeError when _messages has been
cleared.
♻️ Duplicate comments (2)
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx (1)

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

Remove disallowed console.log diagnostics (two sites).

console.log is still present at Line 212 and Line 377. This violates the RN logging rule, and one of these locations was already flagged in prior review.

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

Also applies to: 377-383

🤖 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
212 - 215, Replace disallowed console.log calls with approved logging calls: in
the getGenerationOptions code path (the console.log that prints “[PARAMS] App
getGenerationOptions: …”) change console.log to console.warn or console.error as
appropriate, and also find the other console.log around lines 377–383 in the
ChatScreen component and replace it similarly; ensure both replacements keep the
original message formatting but use console.warn/console.error to comply with
the RN logging rule.

Source: Coding guidelines

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

187-209: ⚠️ Potential issue | 🟠 Major

Make startListening rollback-safe and reentrant-safe.

This still publishes streamRef, taskRef, and isListeningRef before startRecording() succeeds. If mic startup throws, or the button is tapped again before setIsListening(true) runs, the screen can leave an orphaned VAD task behind and stack a second session on top of it.

Suggested fix
   const startListening = async () => {
+    if (isListeningRef.current || isListening) {
+      return;
+    }
     if (!currentModel) {
       Alert.alert('Model Required', 'Please select a VAD model first.');
       return;
@@
     const stream = createPushableAudioStream();
     streamRef.current = stream;
     isListeningRef.current = true;
+    setIsListening(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));
+      });
+    } catch (error) {
+      isListeningRef.current = false;
+      stream.close();
+      streamRef.current = null;
+      taskRef.current = null;
+      setIsListening(false);
+      Alert.alert('VAD Failed', String(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
187 - 209, Prevent reentrancy and orphaned tasks by checking
isListeningRef.current at the top of startListening, creating the pushable
stream and requesting mic permission locally, then await
capture.startRecording() inside a try/catch before assigning shared refs or
launching the VAD task; only after startRecording succeeds set
streamRef.current, isListeningRef.current = true, taskRef.current =
consumeVAD(...), and setIsListening(true). On startRecording failure (catch)
ensure you stop/cleanup the capture if needed, discard the local stream (or
drain/close it), and leave isListeningRef/current/state unchanged (or reset to
false) so no orphaned task remains; reference symbols: startListening,
streamRef, taskRef, isListeningRef, createPushableAudioStream,
capture.startRecording, consumeVAD, setIsListening.
🧹 Nitpick comments (2)
examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx (1)

6-10: ⚡ Quick win

Unify internal imports to tsconfig aliases across MoreScreen.tsx, StorageScreen.tsx, and TabNavigator.tsx.
All three files use relative internal imports where repo policy requires aliases (@/..., @screens/..., @theme/..., @types/...). Please migrate these together so navigation/features stay consistent and future moves are safer.

As per coding guidelines: "Use TypeScript path aliases from tsconfig.json: @/* for src/, @components/, @screens/, @hooks/, @theme/, @types/, @services/, @store/, @utils/* for corresponding src/ subdirectories."

🤖 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/MoreScreen.tsx` around lines
6 - 10, Three files (MoreScreen.tsx, StorageScreen.tsx, TabNavigator.tsx) use
relative internal imports; update them to the project's TypeScript path aliases
so imports are stable. Replace relative imports in MoreScreen.tsx for Symbols
Colors, Typography, Spacing, Padding, BorderRadius and MoreStackParamList with
the corresponding aliases (e.g., `@theme/`* for theme modules and `@types/`* for
MoreStackParamList), and make equivalent changes in StorageScreen.tsx and
TabNavigator.tsx (use `@screens/`*, `@components/`*, `@theme/`*, `@types/`* as
appropriate). Ensure the import module specifiers exactly match the
tsconfig.json paths (@"..." patterns) and run a quick type-check to confirm no
import breaks.

Source: Coding guidelines

examples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dart (1)

17-160: 🏗️ Heavy lift

Move storage/data orchestration into a ChangeNotifier view model.

This screen currently keeps feature-level storage/model-fetch/action state in StatefulWidget. Per project pattern, that logic should live in a singleton ChangeNotifier consumed with ListenableBuilder, while setState stays for strictly local UI-only flags.

As per coding guidelines: "Use ChangeNotifier + ListenableBuilder pattern for feature-level state management ... Use local setState for per-screen UI state."

🤖 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/settings/storage_view.dart`
around lines 17 - 160, Extract feature-level storage state and orchestration out
of _StorageViewState into a singleton ChangeNotifier named StorageViewModel (or
similar) that owns fields _isLoading, _totalUsageBytes, _availableBytes,
_models, _errorMessage and methods refresh(), clearCache(), cleanTempFiles(),
deleteModel(model) and an internal runAction(...) that mirror the logic in
_refresh, _clearCache, _cleanTempFiles, _deleteModel and _runAction; replace all
setState usages that update those feature fields with
StorageViewModel.notifyListeners() updates, make StorageViewModel a singleton
accessor (e.g., StorageViewModel.instance), and update build() to wrap the UI
that depends on that state with a ListenableBuilder that reads
viewModel.isLoading, viewModel.totalUsageBytes, viewModel.availableBytes,
viewModel.models and viewModel.errorMessage; keep only truly local UI-only state
in the State class (use local setState for it) and wire button callbacks to call
the corresponding viewModel methods instead of the private _... methods.

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/app/content_view.dart`:
- Line 12: Restore the full 8-tab navigation surface by ensuring the Scaffold
uses an IndexedStack with eight children and the NavigationBar contains eight
NavigationDestination entries (so all tabs remain mounted); update the build
logic that currently reduces the tab count to five to instead return the
original eight widgets inside the IndexedStack and corresponding destinations in
the NavigationBar, preserving the existing currentIndex state handling and
Scaffold + NavigationBar + IndexedStack pattern (look for IndexedStack,
NavigationBar, Scaffold, currentIndex/currentTab variables and the list of tab
widgets/destinations).

In `@examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart`:
- Around line 50-54: The init flow is missing the Android eager .so preload step
before calling _registerBackends()/RunAnywhere.initialize(); restore
platform-guarded DynamicLibrary.open() calls to preload the six required .so
files (e.g., "libfirst.so", "libsecond.so", etc.) on Android only, executed
before debugPrint('🎯 Initializing SDK...') and _registerBackends(); implement
this in the same init function by checking Platform.isAndroid, opening each of
the six libraries via DynamicLibrary.open() inside a try/catch (log any
failures) and only then proceed to call
_registerBackends()/RunAnywhere.initialize().
- Around line 155-159: The Genie backend is enabled in _registerBackends() but
no Genie model entries are ever registered; update runanywhere_ai_app.dart to
register chip-filtered Genie NPU models during initialization by adding platform
and chip-condition checks (Android + Snapdragon) and calling the existing Genie
model registration API (e.g. Genie.registerModel or similar helper you have)
with the appropriate model entries and chip filters; ensure registration occurs
alongside the existing Genie.register(priority: 200) call so supported
Snapdragon devices receive selectable NPU models.

In `@examples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dart`:
- Around line 124-155: The VAD error and onError paths currently only flip
_isListening and leave resources running; add a single teardown helper (e.g.,
_stopListeningTeardown) that cancels/unsubscribes _vadSubscription and
_levelSubscription and cancels the recorder (_capture.cancel())—using unawaited
where appropriate—and then updates state to set _isListening = false and
_errorMessage when provided; call this helper from the result.errorMessage
branch, from onError, and from onDone (replace the current inline
cancellations/state updates) so all failures always stop the mic stream, level
listener, and recording consistently.

In `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx`:
- Around line 625-629: handleStopGeneration currently clears isLoading
immediately, risking overlapping generation flows; make it async so it awaits
the teardown before clearing loading. Specifically, change handleStopGeneration
to an async function that calls generationAbortRef.current?.abort(), then await
the Promise returned by RunAnywhere.cancelGeneration() (or its completion)
before calling setIsLoading(false); also update the useCallback dependencies to
include setIsLoading (and any other state setters) so React memoization is
correct.
- Around line 505-529: When running on Android and shouldUseTools is false, stop
piping RunAnywhere.generateStream into RunAnywhere.aggregateStream; instead
create a non-stream/manual async-iteration path: call
RunAnywhere.generateStream(prompt, genOptions) to get the async iterator/event
stream, then use a for-await loop to iterate chunks, update accumulatedText and
call updateMessage(...) (same payload used currently), scroll
flatListRef.current?.scrollToEnd(...) after each update and await a short tick
(setTimeout 0) to yield to the UI; handle completion and errors similarly to the
aggregateStream path. Locate symbols RunAnywhere.generateStream,
RunAnywhere.aggregateStream, updateMessage, flatListRef, and shouldUseTools to
add this Android-specific branch and keep the existing streaming/aggregateStream
code for other platforms.

In `@examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx`:
- Around line 38-49: The storage screen lacks error handling for SDK calls and
uses relative imports instead of tsconfig aliases; update the async actions
(refresh, clearCache, cleanTempFiles, and the delete-model handler referenced in
deleteModel) to catch and handle promise rejections: wrap await calls in
try/catch (or add a catch block to the existing try/finally in refresh) and
surface errors via user feedback (set an error state or show an alert) and
ensure refresh() errors are caught when invoked from useEffect (avoid void
refresh() without error handling); also change the import paths from ../theme/*
and ../utils/* to the configured aliases `@theme/`* and `@utils/`* to match
tsconfig.json.

In `@examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx`:
- Around line 623-649: The catch block in consumeLiveTranscription currently
only logs errors which leaves recording state active; modify it to perform
immediate cleanup: call the capture stop/teardown routine (the same logic that
stops the recording session), close or end the push stream used for
transcribeStream without awaiting liveTranscriptionTaskRef.current, set
isRecording to false and reset accumulatedTranscriptRef/current
transcript/partialTranscript/confidence state, and then rethrow or surface the
error to the UI (e.g., set an error state) so the caller or component can show
failure; reference consumeLiveTranscription, liveTranscriptionTaskRef,
accumulatedTranscriptRef, setTranscript, setPartialTranscript, setConfidence and
whatever capture stop/close functions (capture manager / push stream close) are
implemented in this file.

---

Outside diff comments:
In `@examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart`:
- Line 223: The current assistant update logic captures a stale messageIndex and
can write into _messages[messageIndex] after _clearChat() empties the list;
change to capture the message's unique id (e.g., message.id) when creating the
assistant placeholder and, in all streaming and tool-calling update paths (the
code that currently uses messageIndex), lookup the message by id each time
(findIndex or firstWhere on _messages) and abort the update if not found;
alternatively, ensure _clearChat() is disabled while _isGenerating but preferred
fix is lookup-by-id and guard early to avoid RangeError when _messages has been
cleared.

---

Duplicate comments:
In `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx`:
- Around line 212-215: Replace disallowed console.log calls with approved
logging calls: in the getGenerationOptions code path (the console.log that
prints “[PARAMS] App getGenerationOptions: …”) change console.log to
console.warn or console.error as appropriate, and also find the other
console.log around lines 377–383 in the ChatScreen component and replace it
similarly; ensure both replacements keep the original message formatting but use
console.warn/console.error to comply with the RN logging rule.

In `@examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx`:
- Around line 187-209: Prevent reentrancy and orphaned tasks by checking
isListeningRef.current at the top of startListening, creating the pushable
stream and requesting mic permission locally, then await
capture.startRecording() inside a try/catch before assigning shared refs or
launching the VAD task; only after startRecording succeeds set
streamRef.current, isListeningRef.current = true, taskRef.current =
consumeVAD(...), and setIsListening(true). On startRecording failure (catch)
ensure you stop/cleanup the capture if needed, discard the local stream (or
drain/close it), and leave isListeningRef/current/state unchanged (or reset to
false) so no orphaned task remains; reference symbols: startListening,
streamRef, taskRef, isListeningRef, createPushableAudioStream,
capture.startRecording, consumeVAD, setIsListening.

---

Nitpick comments:
In `@examples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dart`:
- Around line 17-160: Extract feature-level storage state and orchestration out
of _StorageViewState into a singleton ChangeNotifier named StorageViewModel (or
similar) that owns fields _isLoading, _totalUsageBytes, _availableBytes,
_models, _errorMessage and methods refresh(), clearCache(), cleanTempFiles(),
deleteModel(model) and an internal runAction(...) that mirror the logic in
_refresh, _clearCache, _cleanTempFiles, _deleteModel and _runAction; replace all
setState usages that update those feature fields with
StorageViewModel.notifyListeners() updates, make StorageViewModel a singleton
accessor (e.g., StorageViewModel.instance), and update build() to wrap the UI
that depends on that state with a ListenableBuilder that reads
viewModel.isLoading, viewModel.totalUsageBytes, viewModel.availableBytes,
viewModel.models and viewModel.errorMessage; keep only truly local UI-only state
in the State class (use local setState for it) and wire button callbacks to call
the corresponding viewModel methods instead of the private _... methods.

In `@examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx`:
- Around line 6-10: Three files (MoreScreen.tsx, StorageScreen.tsx,
TabNavigator.tsx) use relative internal imports; update them to the project's
TypeScript path aliases so imports are stable. Replace relative imports in
MoreScreen.tsx for Symbols Colors, Typography, Spacing, Padding, BorderRadius
and MoreStackParamList with the corresponding aliases (e.g., `@theme/`* for theme
modules and `@types/`* for MoreStackParamList), and make equivalent changes in
StorageScreen.tsx and TabNavigator.tsx (use `@screens/`*, `@components/`*, `@theme/`*,
`@types/`* as appropriate). Ensure the import module specifiers exactly match the
tsconfig.json paths (@"..." patterns) and run a quick type-check to confirm no
import breaks.
🪄 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: cbd9998c-96c9-4448-a30f-9fc2197eb1eb

📥 Commits

Reviewing files that changed from the base of the PR and between 1e115b0 and 1b6953b.

📒 Files selected for processing (49)
  • examples/flutter/RunAnywhereAI/lib/app/content_view.dart
  • examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_status_components.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_types.dart
  • examples/flutter/RunAnywhereAI/lib/features/more/more_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dart
  • examples/react-native/RunAnywhereAI/App.tsx
  • examples/react-native/RunAnywhereAI/README.md
  • examples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsx
  • examples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsx
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.ts
  • examples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/STTScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx
  • examples/react-native/RunAnywhereAI/src/services/VLMService.ts
  • examples/react-native/RunAnywhereAI/src/types/index.ts
  • examples/react-native/RunAnywhereAI/src/types/settings.ts
  • examples/react-native/RunAnywhereAI/src/utils/loraFixture.ts
  • examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts
  • examples/web/RunAnywhereAI/src/components/model-selection.ts
  • examples/web/RunAnywhereAI/src/main.ts
  • examples/web/RunAnywhereAI/src/services/model-catalog.ts
  • examples/web/RunAnywhereAI/src/views/solutions.ts
  • examples/web/RunAnywhereAI/src/views/vision.ts
  • 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/Public/Extensions/LLM/RunAnywhere+TextGeneration.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/EventBus.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/RACErrors.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/SDKException.ts
  • sdk/runanywhere-web/packages/core/src/Foundation/SDKLogger.ts
💤 Files with no reviewable changes (5)
  • examples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsx
  • examples/web/RunAnywhereAI/src/services/model-catalog.ts
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
  • examples/react-native/RunAnywhereAI/src/utils/loraFixture.ts
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
✅ Files skipped from review due to trivial changes (3)
  • examples/flutter/RunAnywhereAI/lib/features/more/more_view.dart
  • examples/web/RunAnywhereAI/src/components/model-selection.ts
  • examples/react-native/RunAnywhereAI/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx

///
/// Main tab-based navigation for the app.
/// Tabs: Chat, Vision, Transcribe (STT), Speak (TTS), Voice, Tools, Settings
/// Tabs: Chat, Vision, Voice, More, Settings

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 | 🟠 Major | 🏗️ Heavy lift

Restore 8-tab main navigation contract.

Line 24 through Line 30 and Line 48 through Line 104 reduce the main IndexedStack/NavigationBar surface to 5 tabs. That regresses required app navigation and unmounts the expected tab set.

As per coding guidelines, Use Scaffold + NavigationBar + IndexedStack pattern for the main app navigation (all 8 tabs stay mounted).

Also applies to: 24-30, 48-104

🤖 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/app/content_view.dart` at line 12, Restore
the full 8-tab navigation surface by ensuring the Scaffold uses an IndexedStack
with eight children and the NavigationBar contains eight NavigationDestination
entries (so all tabs remain mounted); update the build logic that currently
reduces the tab count to five to instead return the original eight widgets
inside the IndexedStack and corresponding destinations in the NavigationBar,
preserving the existing currentIndex state handling and Scaffold + NavigationBar
+ IndexedStack pattern (look for IndexedStack, NavigationBar, Scaffold,
currentIndex/currentTab variables and the list of tab widgets/destinations).

Source: Coding guidelines

Comment on lines 50 to +54
try {
debugPrint('🎯 Initializing SDK...');

await _registerBackends();

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 | 🟠 Major | ⚡ Quick win

Restore the Android .so preload step before backend registration.

This init flow now jumps straight into _registerBackends()/RunAnywhere.initialize() without the required DynamicLibrary.open() preloading step, so Android no longer follows the mandated native-library load order before the first SDK call. As per coding guidelines, examples/flutter/RunAnywhereAI/lib/**/runanywhere_ai_app.dart: “Use eager .so loading (Android only) via DynamicLibrary.open() on 6 .so files to preload before any SDK call during app initialization”.

🤖 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/app/runanywhere_ai_app.dart` around lines
50 - 54, The init flow is missing the Android eager .so preload step before
calling _registerBackends()/RunAnywhere.initialize(); restore platform-guarded
DynamicLibrary.open() calls to preload the six required .so files (e.g.,
"libfirst.so", "libsecond.so", etc.) on Android only, executed before
debugPrint('🎯 Initializing SDK...') and _registerBackends(); implement this in
the same init function by checking Platform.isAndroid, opening each of the six
libraries via DynamicLibrary.open() inside a try/catch (log any failures) and
only then proceed to call _registerBackends()/RunAnywhere.initialize().

Source: Coding guidelines

Comment on lines +155 to +159
if (Genie.isAvailable) {
await Genie.register(priority: 200);
debugPrint(
'✅ Genie backend registered; NPU model catalog is pending generated registry/catalog support',
);

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 | 🟠 Major | 🏗️ Heavy lift

Genie is enabled, but no chip-filtered Genie models are ever registered.

_registerBackends() turns on the Genie backend, but the rest of this file never registers any Genie model entries, so supported Snapdragon devices still have no NPU models to select or load. As per coding guidelines, examples/flutter/RunAnywhereAI/lib/**/runanywhere_ai_app.dart: “Register ... Genie NPU (Android/Snapdragon only with chip-conditional models) ... during initialization”.

Also applies to: 195-457

🤖 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/app/runanywhere_ai_app.dart` around lines
155 - 159, The Genie backend is enabled in _registerBackends() but no Genie
model entries are ever registered; update runanywhere_ai_app.dart to register
chip-filtered Genie NPU models during initialization by adding platform and
chip-condition checks (Android + Snapdragon) and calling the existing Genie
model registration API (e.g. Genie.registerModel or similar helper you have)
with the appropriate model entries and chip filters; ensure registration occurs
alongside the existing Genie.register(priority: 200) call so supported
Snapdragon devices receive selectable NPU models.

Source: Coding guidelines

Comment on lines +124 to +155
_vadSubscription = sdk.RunAnywhere.vad
.streamVAD(chunks)
.listen(
(result) {
if (!mounted) return;
if (result.errorMessage.isNotEmpty) {
setState(() {
_errorMessage = result.errorMessage;
_isListening = false;
});
unawaited(_capture.cancel());
return;
}
setState(() {
_isSpeech = result.isSpeech;
_confidence = result.confidence;
_energy = result.energy;
_frameCount += 1;
});
},
onError: (Object e) {
if (!mounted) return;
setState(() {
_errorMessage = 'VAD failed: $e';
_isListening = false;
});
},
onDone: () {
if (!mounted) return;
setState(() => _isListening = false);
},
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop capture and both subscriptions on VAD stream failure.

The result.errorMessage and onError paths only flip _isListening, so a failed streamVAD() session can keep the mic stream and/or level listener alive after the UI says it stopped. Route both branches through one teardown helper that cancels _vadSubscription, _levelSubscription, and recording before updating state.

Suggested fix
+  Future<void> _handleListeningFailure(String message) async {
+    await _vadSubscription?.cancel();
+    _vadSubscription = null;
+    await _levelSubscription?.cancel();
+    _levelSubscription = null;
+    await _capture.cancel();
+    if (!mounted) return;
+    setState(() {
+      _errorMessage = message;
+      _isListening = false;
+      _audioLevel = 0;
+    });
+  }
+
     _vadSubscription = sdk.RunAnywhere.vad
         .streamVAD(chunks)
         .listen(
           (result) {
             if (!mounted) return;
             if (result.errorMessage.isNotEmpty) {
-              setState(() {
-                _errorMessage = result.errorMessage;
-                _isListening = false;
-              });
-              unawaited(_capture.cancel());
+              unawaited(_handleListeningFailure(result.errorMessage));
               return;
             }
             setState(() {
               _isSpeech = result.isSpeech;
               _confidence = result.confidence;
@@
-          onError: (Object e) {
-            if (!mounted) return;
-            setState(() {
-              _errorMessage = 'VAD failed: $e';
-              _isListening = false;
-            });
-          },
+          onError: (Object e) =>
+              unawaited(_handleListeningFailure('VAD 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/voice/vad_view.dart` around lines
124 - 155, The VAD error and onError paths currently only flip _isListening and
leave resources running; add a single teardown helper (e.g.,
_stopListeningTeardown) that cancels/unsubscribes _vadSubscription and
_levelSubscription and cancels the recorder (_capture.cancel())—using unawaited
where appropriate—and then updates state to set _isListening = false and
_errorMessage when provided; call this helper from the result.errorMessage
branch, from onError, and from onDone (replace the current inline
cancellations/state updates) so all failures always stop the mic stream, level
listener, and recording consistently.

Comment on lines +505 to +529
const eventStream = RunAnywhere.generateStream(prompt, genOptions);
const result = await RunAnywhere.aggregateStream(
prompt,
eventStream,
async (transcript) => {
accumulatedText = transcript;
updateMessage(
{
id: assistantMessageId,
role: MessageRole.Assistant,
content: accumulatedText,
timestamp: new Date(),
modelInfo: {
modelId: currentModel?.id || 'unknown',
modelName: currentModel?.name || 'Unknown Model',
framework: frameworkName,
frameworkDisplayName: frameworkName,
},
},
currentConversation.id
);
flatListRef.current?.scrollToEnd({ animated: false });
await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
);

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "== Snippet around lines 480-560 =="
nl -ba "$FILE" | sed -n '480,560p'

echo
echo "== Search within ChatScreen.tsx =="
rg -n --hidden "generateStream|aggregateStream|Platform\.OS|android|__ANDROID__" "$FILE" || true

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "== Snippet around lines 480-560 =="
awk 'NR>=480 && NR<=560 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo
echo "== Search within ChatScreen.tsx =="
if command -v rg >/dev/null 2>&1; then
  rg -n --hidden "generateStream|aggregateStream|Platform\.OS|android|__ANDROID__" "$FILE" || true
else
  grep -nE "generateStream|aggregateStream|Platform\.OS|android|__ANDROID__" "$FILE" || true
fi

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 4035


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx"

echo "== Conditional context around lines 430-520 =="
awk 'NR>=380 && NR<=540 {printf "%d\t%s\n", NR, $0}' "$FILE"

echo
echo "== Search for platform-specific branching in this file =="
if command -v rg >/dev/null 2>&1; then
  rg -n --hidden "Platform\.OS|isAndroid|android|NativeAudioModule|react-native-sound" "$FILE" || true
else
  grep -nE "Platform\.OS|isAndroid|android|NativeAudioModule|react-native-sound" "$FILE" || true
fi

echo
echo "== Search for non-stream generation usage in ChatScreen =="
if command -v rg >/dev/null 2>&1; then
  rg -n --hidden "generate\\w*\\(|aggregate\\w*\\(|generateStream|aggregateStream" "$FILE" || true
else
  grep -nE "generate\\w*\\(|aggregate\\w*\\(|generateStream|aggregateStream" "$FILE" || true
fi

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 7389


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx"

echo "== Search for Platform usage =="
if command -v rg >/dev/null 2>&1; then
  rg -n "Platform" "$FILE" || true
else
  grep -n "Platform" "$FILE" || true
fi

echo
echo "== Search for Android usage (case-insensitive) =="
if command -v rg >/dev/null 2>&1; then
  rg -n --ignore-case "android" "$FILE" || true
else
  grep -n -i "android" "$FILE" || true
fi

echo
echo "== Search for aggregateStream/generateStream usages =="
if command -v rg >/dev/null 2>&1; then
  rg -n "generateStream\(|aggregateStream\(" "$FILE" || true
else
  grep -nE "generateStream\\(|aggregateStream\\(" "$FILE" || true
fi

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 595


Android: add non-stream/manual async iteration path for RunAnywhere.generateStream() in ChatScreen

In examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx, when shouldUseTools is false the code always uses the streaming path (genOptions.streamingEnabled: true + RunAnywhere.generateStream(...) piped into RunAnywhere.aggregateStream(...), lines ~397-415 and ~505-529). Coding guidelines require an Android-specific non-stream/manual async iteration path for RunAnywhere.generateStream() in ChatScreen instead of this streaming approach.

🤖 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
505 - 529, When running on Android and shouldUseTools is false, stop piping
RunAnywhere.generateStream into RunAnywhere.aggregateStream; instead create a
non-stream/manual async-iteration path: call RunAnywhere.generateStream(prompt,
genOptions) to get the async iterator/event stream, then use a for-await loop to
iterate chunks, update accumulatedText and call updateMessage(...) (same payload
used currently), scroll flatListRef.current?.scrollToEnd(...) after each update
and await a short tick (setTimeout 0) to yield to the UI; handle completion and
errors similarly to the aggregateStream path. Locate symbols
RunAnywhere.generateStream, RunAnywhere.aggregateStream, updateMessage,
flatListRef, and shouldUseTools to add this Android-specific branch and keep the
existing streaming/aggregateStream code for other platforms.

Source: Coding guidelines

Comment on lines +625 to +629
const handleStopGeneration = useCallback(() => {
generationAbortRef.current?.abort();
void RunAnywhere.cancelGeneration();
setIsLoading(false);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t clear loading state before generation teardown completes.

Line 628 sets isLoading to false immediately in handleStopGeneration, which can let users send a second prompt while the first handleSend flow is still finalizing message updates. That creates overlapping generation/update races.

Suggested fix
  const handleStopGeneration = useCallback(() => {
    generationAbortRef.current?.abort();
    void RunAnywhere.cancelGeneration();
-   setIsLoading(false);
  }, []);
📝 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 handleStopGeneration = useCallback(() => {
generationAbortRef.current?.abort();
void RunAnywhere.cancelGeneration();
setIsLoading(false);
}, []);
const handleStopGeneration = useCallback(() => {
generationAbortRef.current?.abort();
void RunAnywhere.cancelGeneration();
}, []);
🤖 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
625 - 629, handleStopGeneration currently clears isLoading immediately, risking
overlapping generation flows; make it async so it awaits the teardown before
clearing loading. Specifically, change handleStopGeneration to an async function
that calls generationAbortRef.current?.abort(), then await the Promise returned
by RunAnywhere.cancelGeneration() (or its completion) before calling
setIsLoading(false); also update the useCallback dependencies to include
setIsLoading (and any other state setters) so React memoization is correct.

Comment on lines +38 to +49
const refresh = useCallback(async () => {
setIsRefreshing(true);
try {
const [storage, models] = await Promise.all([
RunAnywhere.getStorageInfo(),
RunAnywhere.downloadedModels(),
]);
setStorageInfo(storage);
setDownloadedModels(models.models?.models ?? []);
} finally {
setIsRefreshing(false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx"

echo "== File existence =="
ls -la "$FILE" || true

echo "== Imports (top of file) =="
sed -n '1,120p' "$FILE"

echo "== refresh / related handlers (around referenced lines) =="
sed -n '30,120p' "$FILE"

echo "== Full async actions (wider context) =="
sed -n '120,260p' "$FILE"

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 10603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -ლა tsconfig*.json || true
for f in tsconfig*.json; do
  [ -f "$f" ] || continue
  echo "==== $f ===="
  rg -n '"paths"\s*:' -n "$f" || true
  # Print the compilerOptions block containing paths (small extract)
  python3 - <<'PY'
import json,sys
p=sys.argv[1]
with open(p) as f: data=json.load(f)
paths=((data.get("compilerOptions") or {}).get("paths")) or {}
print("paths keys:", ", ".join(paths.keys()) if paths else "<none>")
# Print relevant entries containing theme or utils
for k,v in paths.items():
  if any(s in k.lower() for s in ["theme","utils","screens","components","hooks","types","services","store","`@/`"]):
    print(k, "=>", v)
PY "$f"
done

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 292


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la tsconfig*.json || true
for f in tsconfig*.json; do
  [ -f "$f" ] || continue
  echo "==== $f ===="
  python3 - <<'PY' "$f"
import json,sys
p=sys.argv[1]
with open(p) as fp:
  data=json.load(fp)
paths=((data.get("compilerOptions") or {}).get("paths")) or {}
print("paths keys:", ", ".join(paths.keys()) if paths else "<none>")
for k,v in paths.items():
  lk=k.lower()
  if any(s in lk for s in ["theme","utils","screens","components","hooks","types","services","store","`@/`","`@theme`","`@utils`","`@screens`","`@components`","`@hooks`","`@types`","`@services`","`@store`"]):
    print(k, "=>", v)
PY
done

# Also search for other tsconfig variants (optional)
if [ -d . ]; then
  fd -t f -g 'tsconfig*.json' . -a | head -n 50
fi

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1192


🏁 Script executed:

#!/bin/bash
set -euo pipefail

TS="examples/react-native/RunAnywhereAI/tsconfig.json"
echo "== $TS =="
python3 - <<'PY' "$TS"
import json,sys
p=sys.argv[1]
with open(p) as f: data=json.load(f)
co=data.get("compilerOptions") or {}
paths=co.get("paths") or {}
print("paths keys:", ", ".join(paths.keys()) if paths else "<none>")
# print relevant alias mappings
for k,v in paths.items():
    if k.startswith("@") or any(s in k.lower() for s in ["theme","utils","screens","components","hooks","types","services","store"]):
        print(f"{k} => {v}")
PY

# Also sanity-check if StorageScreen imports use any of these aliases
echo "== StorageScreen current relative theme/utils imports =="
sed -n '1,40p' examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2005


Handle SDK failures in storage actions and fix tsconfig path aliases in StorageScreen.tsx

  • refresh uses try/finally without a catch, and it’s invoked via void refresh() in useEffect, so SDK rejections can become unhandled with no user-visible error (lines 38-49).
  • clearCache, cleanTempFiles, and the delete-model async path similarly lack local error handling (lines 56-64, 73-87).
  • Theme/utils imports use relative paths (../theme/*, ../utils/*) instead of required aliases from tsconfig.json (@theme/*, @utils/*).
💡 Minimal hardening pattern
-  const refresh = useCallback(async () => {
+  const refresh = useCallback(async () => {
     setIsRefreshing(true);
     try {
       const [storage, models] = await Promise.all([
         RunAnywhere.getStorageInfo(),
         RunAnywhere.downloadedModels(),
       ]);
       setStorageInfo(storage);
       setDownloadedModels(models.models?.models ?? []);
+    } catch (e) {
+      Alert.alert('Storage Error', String(e));
     } finally {
       setIsRefreshing(false);
     }
   }, []);
📝 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 refresh = useCallback(async () => {
setIsRefreshing(true);
try {
const [storage, models] = await Promise.all([
RunAnywhere.getStorageInfo(),
RunAnywhere.downloadedModels(),
]);
setStorageInfo(storage);
setDownloadedModels(models.models?.models ?? []);
} finally {
setIsRefreshing(false);
}
const refresh = useCallback(async () => {
setIsRefreshing(true);
try {
const [storage, models] = await Promise.all([
RunAnywhere.getStorageInfo(),
RunAnywhere.downloadedModels(),
]);
setStorageInfo(storage);
setDownloadedModels(models.models?.models ?? []);
} catch (e) {
Alert.alert('Storage Error', String(e));
} finally {
setIsRefreshing(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 `@examples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsx` around
lines 38 - 49, The storage screen lacks error handling for SDK calls and uses
relative imports instead of tsconfig aliases; update the async actions (refresh,
clearCache, cleanTempFiles, and the delete-model handler referenced in
deleteModel) to catch and handle promise rejections: wrap await calls in
try/catch (or add a catch block to the existing try/finally in refresh) and
surface errors via user feedback (set an error state or show an alert) and
ensure refresh() errors are caught when invoked from useEffect (avoid void
refresh() without error handling); also change the import paths from ../theme/*
and ../utils/* to the configured aliases `@theme/`* and `@utils/`* to match
tsconfig.json.

Comment on lines +623 to 649
const consumeLiveTranscription = async (
partials: AsyncIterable<STTPartialResult>
) => {
const iterator = partials[Symbol.asyncIterator]();
try {
console.warn('[STTScreen] Transcribing live chunk...');
setPartialTranscript('Processing...');

// Stop current recording
const { uri: resultPath } = await AudioRecorder.stopRecording();

// Get the path
let audioPath = resultPath;
if (audioPath.startsWith('file://')) {
audioPath = audioPath.replace('file://', '');
}

// Check file exists
const exists = await RNFS.exists(audioPath);
if (!exists) {
console.warn('[STTScreen] Live chunk file not found');
setPartialTranscript('Listening...');
if (isLiveRecordingRef.current) {
await startLiveChunk();
}
return;
}

// Check file size (skip very small files)
const stat = await RNFS.stat(audioPath);
if (stat.size < 5000) {
console.warn('[STTScreen] Chunk too small, skipping transcription');
setPartialTranscript('Listening...');
if (isLiveRecordingRef.current) {
await startLiveChunk();
}
return;
}

// Transcribe using native module (handles audio decoding)
const result = await transcribeAudioFile(audioPath);
console.warn('[STTScreen] Live chunk transcription:', result.text);

// Append to accumulated transcript if we got text
if (result.text && result.text.trim() && result.text.trim() !== '') {
const newText = result.text.trim();
if (accumulatedTranscriptRef.current) {
accumulatedTranscriptRef.current += ' ' + newText;
} else {
accumulatedTranscriptRef.current = newText;
let step = await iterator.next();
while (!step.done) {
const partial = step.value;
const finalText = partial.finalOutput?.text?.trim();
const text = (finalText || partial.text || '').trim();
if (text) {
if (partial.isFinal || finalText) {
accumulatedTranscriptRef.current = text;
setTranscript(text);
setPartialTranscript('');
setConfidence(partial.finalOutput?.confidence ?? null);
} else {
setPartialTranscript(text);
}
}
setTranscript(accumulatedTranscriptRef.current);
setConfidence(result.confidence || null);
}

// Clean up chunk file
await RNFS.unlink(audioPath).catch(() => {});

// Update partial transcript for next chunk
setPartialTranscript('Listening...');

// Start next chunk if still recording
if (isLiveRecordingRef.current) {
await startLiveChunk();
step = await iterator.next();
}
} catch (error) {
console.error('[STTScreen] Error transcribing live chunk:', error);
setPartialTranscript('Listening...');
// Try to continue with next chunk
if (isLiveRecordingRef.current) {
await startLiveChunk();
}
console.error('[STTScreen] Live transcription stream error:', error);
} finally {
await iterator.return?.();
}

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 | 🟠 Major | ⚡ Quick win

Live transcription stream errors leave the recording session stuck.

When transcribeStream() throws, this catch block only logs the error. isRecording stays true, the capture manager keeps running, and the user gets no failure signal until they manually stop. Extract a cleanup path that stops capture/closes the push stream without awaiting liveTranscriptionTaskRef.current, then reset state and surface the 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/STTScreen.tsx` around lines
623 - 649, The catch block in consumeLiveTranscription currently only logs
errors which leaves recording state active; modify it to perform immediate
cleanup: call the capture stop/teardown routine (the same logic that stops the
recording session), close or end the push stream used for transcribeStream
without awaiting liveTranscriptionTaskRef.current, set isRecording to false and
reset accumulatedTranscriptRef/current transcript/partialTranscript/confidence
state, and then rethrow or surface the error to the UI (e.g., set an error
state) so the caller or component can show failure; reference
consumeLiveTranscription, liveTranscriptionTaskRef, accumulatedTranscriptRef,
setTranscript, setPartialTranscript, setConfidence and whatever capture
stop/close functions (capture manager / push stream close) are implemented in
this file.

- RN SDK: layer model-type lookups into the C ABI via sync Nitro thunks
  (rac_inference_framework_display_name, rac_model_category_default_framework,
  rac_infer_model_file_role); embeddings rewritten to the handle-less
  rac_embeddings_embed_batch_lifecycle_proto flow; remove RN-only public APIs
  (defaultVoiceAgentComposeConfig, getStorageInfoProto, lora.attachAdapter);
  ship createPushableAudioStream as an SDK helper
- RN example: catalog extracted to ModelCatalogBootstrap with LoRA seed and
  qwen3-0.6b memory aligned to iOS; LoRA management sheet in Chat; Benchmarks
  screen under Settings (LLM/STT/TTS); RAG passes disableThinking; STT/VAD
  use the SDK pushable stream; stale comments fixed
- iOS example: catalog superset adds lfm2.5-1.2b-instruct and
  llama-3.2-3b-instruct
- Flutter example: matching catalog bootstrap, LoRA sheet, benchmarks, and
  voice view-models

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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

🧹 Nitpick comments (5)
examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart (1)

76-78: ⚡ Quick win

Remove redundant .toInt() call.

The clamp() method on int already returns an int, so the trailing .toInt() on line 78 is redundant.

♻️ Simplify the expression
-        _maxTokens = (prefs.getInt(PreferenceKeys.defaultMaxTokens) ?? 1000)
-            .clamp(500, 20000)
-            .toInt();
+        _maxTokens = (prefs.getInt(PreferenceKeys.defaultMaxTokens) ?? 1000)
+            .clamp(500, 20000);
🤖 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/settings/combined_settings_view.dart`
around lines 76 - 78, The expression assigning _maxTokens uses
(prefs.getInt(PreferenceKeys.defaultMaxTokens) ?? 1000).clamp(500,
20000).toInt(); remove the redundant .toInt() call because clamp on int already
returns an int; update the assignment to use .clamp(500, 20000) directly (e.g.,
assign the result of clamp to _maxTokens) so only _maxTokens, prefs.getInt, and
PreferenceKeys.defaultMaxTokens are touched.
examples/flutter/RunAnywhereAI/lib/features/benchmarks/tts_benchmark_provider.dart (1)

52-59: TTS duration fallback matches SDK defaults; hardcoding Float32 could be made more robust

  • result.durationMs in the SDK is already an Int64 millisecond value, so result.durationMs.toInt() isn’t a double→int precision-loss issue.
  • For the default tts.synthesize() path, the SDK sets audioFormat to AUDIO_FORMAT_PCM, and the SDK’s speak() converts TTSOutput.audioData using rac_audio_float32_to_wav (Float32 PCM), which aligns with the “/4 bytes per sample” fallback.
  • Robustness: if audioFormat can vary (e.g., AUDIO_FORMAT_PCM_S16LE), prefer using result.audioFormat to choose bytes/sample instead of assuming Float32.
      final durationMs = result.durationMs.toInt();
      if (durationMs > 0) {
        metrics.audioDurationSeconds = durationMs / 1000.0;
      } else if (result.sampleRate > 0 && result.audioData.isNotEmpty) {
        // Fallback: Float32 PCM — 4 bytes per sample.
        metrics.audioDurationSeconds =
            (result.audioData.length / 4) / result.sampleRate;
      }
🤖 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/benchmarks/tts_benchmark_provider.dart`
around lines 52 - 59, The fallback that computes metrics.audioDurationSeconds
assumes Float32 PCM (4 bytes/sample); update the logic that currently uses
result.durationMs, result.sampleRate, result.audioData to inspect
result.audioFormat and pick bytes-per-sample based on that format (e.g., Float32
-> 4, S16LE -> 2, default/unknown -> conservative 1 or skip) before computing
(audioData.length / bytesPerSample) / sampleRate; modify the branch around
result.durationMs.toInt() and the else-if that sets metrics.audioDurationSeconds
so it consults result.audioFormat and handles unknown formats safely.
examples/react-native/RunAnywhereAI/App.tsx (1)

201-231: 💤 Low value

Replace console.log with console.warn for diagnostic messages.

Per coding guidelines, only console.warn and console.error are allowed. While the eslint-disable comment acknowledges this for "demo app bootstrap diagnostics", consider using console.warn or the logDiagnostic utility (already used elsewhere in this file) for consistency.

       if (
         hasCustomConfig &&
         customApiKey &&
         customBaseURL &&
         hasUsableBackendConfig({ apiKey: customApiKey, baseURL: customBaseURL })
       ) {
-        console.log('[App] Found custom API configuration');
+        console.warn('[App] Found custom API configuration');
         await RunAnywhere.initialize({
           apiKey: customApiKey,
           baseURL: customBaseURL,
           environment: SDKEnvironment.SDK_ENVIRONMENT_PRODUCTION,
         });
-        console.log(
+        console.warn(
           '[App] SDK initialized with custom configuration (production)'
         );
       } else {
         await RunAnywhere.initialize({
           apiKey: '',
           baseURL: 'https://api.runanywhere.ai',
           environment: SDKEnvironment.SDK_ENVIRONMENT_DEVELOPMENT,
         });
-        console.log('[App] SDK initialized in DEVELOPMENT mode');
+        console.warn('[App] SDK initialized in DEVELOPMENT mode');
       }
       // ...
-      console.log(
+      console.warn(
         `[App] SDK initialized: v${version}, ${isInit ? 'Active' : 'Inactive'}, ${initTime}ms, state: ${JSON.stringify(sdkState)}`
       );
🤖 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 201 - 231, The
console.log diagnostic messages in the SDK bootstrap block should be replaced
with allowed logging (console.warn or the existing logDiagnostic helper) to
comply with lint rules: update all occurrences of console.log in the
initialization sequence (around RunAnywhere.initialize, the else branch, after
registerAll(backendState), after RunAnywhere.refreshModelRegistry(), and the
final SDK status log that references RunAnywhere.isInitialized,
RunAnywhere.version and sdkState) to use console.warn or call logDiagnostic(...)
consistently.

Source: Coding guidelines

examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx (1)

37-39: 💤 Low value

Consider using TypeScript path aliases for theme imports.

Per coding guidelines, prefer @theme/* path aliases over relative paths.

-import { Colors } from '../../theme/colors';
-import { Typography } from '../../theme/typography';
-import { Spacing, Padding, BorderRadius } from '../../theme/spacing';
+import { Colors } from '`@theme/colors`';
+import { Typography } from '`@theme/typography`';
+import { Spacing, Padding, BorderRadius } from '`@theme/spacing`';
🤖 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 37 - 39, Replace the relative theme imports in LoRASheet.tsx (the import
statements that bring in Colors, Typography, and Spacing/Padding/BorderRadius)
with the project TypeScript path aliases (e.g., use `@theme/colors`,
`@theme/typography`, `@theme/spacing` or the alias convention used in
tsconfig.json); update the import specifiers for Colors, Typography, Spacing,
Padding, and BorderRadius accordingly and ensure the tsconfig/metro config has
the corresponding path mappings so the new aliases resolve at build/runtime.

Source: Coding guidelines

examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart (1)

453-488: 💤 Low value

Consider consistent placeholder pattern for non-streaming generation.

The streaming and tool-calling paths append an empty assistant message first, then update it at a fixed index. The non-streaming path creates and appends the message after generation completes. While functionally correct, this inconsistency means the UI won't show a placeholder during non-streaming generation.

🤖 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/chat/chat_view_model.dart` around
lines 453 - 488, In _generateNonStreaming: make the non-streaming path match the
streaming/tool-calling pattern by appending an empty assistant ChatMessage
placeholder to _messages and calling notifyListeners before calling
sdk.RunAnywhere.llm.generate, then after getting result update that same
placeholder (use the index e.g. last index) to set content/result.text,
thinkingContent, timestamp and MessageAnalytics, call _persistMessage for that
updated message, clear _isGenerating and notifyListeners; also ensure the catch
block updates or replaces the placeholder with the error state/message so the UI
placeholder is cleared consistently.
🤖 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/core/utilities/keychain_helper.dart`:
- Line 17: KeychainHelper stopped prefixing keys (via _prefixKey) before calling
KeychainService.shared which breaks existing entries; restore the old behavior
or add a migration/fallback: ensure write methods (e.g.,
KeychainHelper.saveBytes / saveString) call
KeychainService.shared.saveBytes/saveString with _prefixKey(key) (i.e.,
'${_service}_$key') and read methods (e.g., KeychainHelper.readBytes /
readString) first attempt to read using _prefixKey(key) and if missing try the
raw key and, on success, optionally copy the prefixed value to the raw key or
vice versa so existing prefixed entries remain readable.‬

In `@examples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_types.dart`:
- Around line 282-298: BenchmarkResult.fromJson assigns DateTime.tryParse(...)
directly to the non-nullable BenchmarkResult.timestamp; change it to use a safe
fallback when parsing fails (e.g. use the parsed value if non-null, otherwise
use a sensible default such as DateTime.now() or epoch). Update the timestamp
expression in BenchmarkResult.fromJson so it sets timestamp to
DateTime.tryParse(json['timestamp'] as String? ?? '') ?? DateTime.now() (or your
chosen default) to avoid a null assignment to the timestamp field.
- Around line 333-347: The fromJson factory in BenchmarkRun uses
DateTime.tryParse for startedAt and completedAt which can return null but the
constructor fields startedAt and completedAt are non-nullable; update
BenchmarkRun.fromJson to provide a non-null fallback (e.g., replace
DateTime.tryParse(json['startedAt'] as String? ?? '') with
DateTime.tryParse(...) ?? DateTime.fromMillisecondsSinceEpoch(0)) or validate
and throw a clear FormatException before constructing; ensure both startedAt and
completedAt are handled and keep references to BenchmarkRun.fromJson, startedAt,
and completedAt so the change is applied to those exact fields.

In `@examples/flutter/RunAnywhereAI/lib/features/voice/vad_view_model.dart`:
- Around line 112-136: The VAD failure paths duplicate partial teardown and
leave _vadSubscription and _levelSubscription active; create a single helper
method (e.g., _teardownVAD or _stopVADSession) that 1) cancels/pauses both
_vadSubscription and _levelSubscription, 2) stops recording by awaiting or
unawaiting _capture.cancel(), and 3) sets isListening=false, clears/sets
errorMessage and calls notify(); then replace the inline cleanup in the
RunAnywhere.vad.streamVAD listener (the result.errorMessage branch), the onError
handler, and the onDone handler to call this helper so all failure/completion
paths perform the same consolidated teardown.

In
`@examples/flutter/RunAnywhereAI/lib/features/voice/voice_component_view_model_base.dart`:
- Around line 119-134: The event filter in the _lifecycleSubscription callback
for sdk.RunAnywhere.events.modelLifecycle.listen is using && so it only returns
when both change.component != component and change.event.category !=
eventCategory; change the condition to use || so the callback returns (skips the
event) if either the component or the eventCategory doesn't match. Update the
check referencing change.component, component, change.event.category and
eventCategory accordingly to ensure only events for this specific
component/category are processed.

---

Nitpick comments:
In
`@examples/flutter/RunAnywhereAI/lib/features/benchmarks/tts_benchmark_provider.dart`:
- Around line 52-59: The fallback that computes metrics.audioDurationSeconds
assumes Float32 PCM (4 bytes/sample); update the logic that currently uses
result.durationMs, result.sampleRate, result.audioData to inspect
result.audioFormat and pick bytes-per-sample based on that format (e.g., Float32
-> 4, S16LE -> 2, default/unknown -> conservative 1 or skip) before computing
(audioData.length / bytesPerSample) / sampleRate; modify the branch around
result.durationMs.toInt() and the else-if that sets metrics.audioDurationSeconds
so it consults result.audioFormat and handles unknown formats safely.

In `@examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart`:
- Around line 453-488: In _generateNonStreaming: make the non-streaming path
match the streaming/tool-calling pattern by appending an empty assistant
ChatMessage placeholder to _messages and calling notifyListeners before calling
sdk.RunAnywhere.llm.generate, then after getting result update that same
placeholder (use the index e.g. last index) to set content/result.text,
thinkingContent, timestamp and MessageAnalytics, call _persistMessage for that
updated message, clear _isGenerating and notifyListeners; also ensure the catch
block updates or replaces the placeholder with the error state/message so the UI
placeholder is cleared consistently.

In
`@examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart`:
- Around line 76-78: The expression assigning _maxTokens uses
(prefs.getInt(PreferenceKeys.defaultMaxTokens) ?? 1000).clamp(500,
20000).toInt(); remove the redundant .toInt() call because clamp on int already
returns an int; update the assignment to use .clamp(500, 20000) directly (e.g.,
assign the result of clamp to _maxTokens) so only _maxTokens, prefs.getInt, and
PreferenceKeys.defaultMaxTokens are touched.

In `@examples/react-native/RunAnywhereAI/App.tsx`:
- Around line 201-231: The console.log diagnostic messages in the SDK bootstrap
block should be replaced with allowed logging (console.warn or the existing
logDiagnostic helper) to comply with lint rules: update all occurrences of
console.log in the initialization sequence (around RunAnywhere.initialize, the
else branch, after registerAll(backendState), after
RunAnywhere.refreshModelRegistry(), and the final SDK status log that references
RunAnywhere.isInitialized, RunAnywhere.version and sdkState) to use console.warn
or call logDiagnostic(...) consistently.

In `@examples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsx`:
- Around line 37-39: Replace the relative theme imports in LoRASheet.tsx (the
import statements that bring in Colors, Typography, and
Spacing/Padding/BorderRadius) with the project TypeScript path aliases (e.g.,
use `@theme/colors`, `@theme/typography`, `@theme/spacing` or the alias convention
used in tsconfig.json); update the import specifiers for Colors, Typography,
Spacing, Padding, and BorderRadius accordingly and ensure the tsconfig/metro
config has the corresponding path mappings so the new aliases resolve at
build/runtime.
🪄 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: 771749ee-e9b8-4b48-b94f-b4e3dcd90ba6

📥 Commits

Reviewing files that changed from the base of the PR and between 1e115b0 and 7985451.

⛔ Files ignored due to path filters (3)
  • examples/flutter/RunAnywhereAI/lib/generated/solutions_yaml.dart is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.hpp is excluded by !**/generated/**
📒 Files selected for processing (104)
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vision/VisionViewModel.kt
  • examples/flutter/RunAnywhereAI/README.md
  • examples/flutter/RunAnywhereAI/lib/app/content_view.dart
  • examples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dart
  • examples/flutter/RunAnywhereAI/lib/core/services/conversation_store.dart
  • examples/flutter/RunAnywhereAI/lib/core/services/model_catalog_bootstrap.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/constants.dart
  • examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_dashboard_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_runner.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_store.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_types.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/llm_benchmark_provider.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/stt_benchmark_provider.dart
  • examples/flutter/RunAnywhereAI/lib/features/benchmarks/tts_benchmark_provider.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_lora_sheet.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/models/model_status_components.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_types.dart
  • examples/flutter/RunAnywhereAI/lib/features/more/more_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/solutions/solutions_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/speech_to_text_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/stt_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/text_to_speech_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/tts_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/voice/vad_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/flutter/RunAnywhereAI/lib/features/voice/voice_component_view_model_base.dart
  • examples/flutter/RunAnywhereAI/pubspec.yaml
  • examples/flutter/RunAnywhereAI/scripts/sync-solutions-yamls.sh
  • examples/flutter/RunAnywhereAI/scripts/verify.sh
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Core/Services/ModelCatalogBootstrap.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Vision/VLMViewModel.swift
  • examples/react-native/RunAnywhereAI/App.tsx
  • examples/react-native/RunAnywhereAI/README.md
  • 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/index.ts
  • examples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsx
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.ts
  • examples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsx
  • examples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ChatScreen.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/StorageScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VADScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsx
  • examples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsx
  • examples/react-native/RunAnywhereAI/src/services/ModelCatalogBootstrap.ts
  • examples/react-native/RunAnywhereAI/src/services/VLMService.ts
  • examples/react-native/RunAnywhereAI/src/types/index.ts
  • examples/react-native/RunAnywhereAI/src/types/settings.ts
  • examples/react-native/RunAnywhereAI/src/utils/chatSampleTools.ts
  • examples/react-native/RunAnywhereAI/src/utils/loraFixture.ts
  • examples/react-native/RunAnywhereAI/src/utils/modelDisplay.ts
  • examples/react-native/RunAnywhereAI/src/utils/syntheticAudio.ts
  • examples/web/RunAnywhereAI/src/components/model-selection.ts
  • examples/web/RunAnywhereAI/src/main.ts
  • examples/web/RunAnywhereAI/src/services/model-catalog.ts
  • examples/web/RunAnywhereAI/src/views/solutions.ts
  • examples/web/RunAnywhereAI/src/views/vision.ts
  • idl/codegen/swift-modality-abi.yaml
  • sdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_component.h
  • sdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_service.h
  • sdk/runanywhere-commons/src/core/model_lifecycle.cpp
  • sdk/runanywhere-commons/src/core/model_lifecycle_resolve.cpp
  • sdk/runanywhere-commons/src/features/common/rac_service_factory_internal.h
  • sdk/runanywhere-commons/src/features/vlm/vlm_module.cpp
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
  • sdk/runanywhere-commons/tests/test_advanced_modality_proto_abi.cpp
  • sdk/runanywhere-flutter/packages/runanywhere/lib/foundation/constants/sdk_constants.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_llm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tools.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vlm.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVLM.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/VlmStreamEventMapper.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VLM/RunAnywhereVisionLanguage.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/types/SwiftAliases.kt
  • sdk/runanywhere-kotlin/src/test/kotlin/com/runanywhere/sdk/public/extensions/VLM/VLMGeneratedStreamSurfaceTest.kt
  • sdk/runanywhere-react-native/AGENTS.md
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+ProtoCompat.hpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Registry.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Tools.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.hpp
💤 Files with no reviewable changes (2)
  • examples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart
✅ Files skipped from review due to trivial changes (3)
  • examples/flutter/RunAnywhereAI/scripts/verify.sh
  • examples/flutter/RunAnywhereAI/README.md
  • examples/react-native/RunAnywhereAI/README.md

key: _prefixKey(key),
data: bytes,
);
await KeychainService.shared.saveBytes(key: key, data: bytes);

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for historical _prefixKey implementation and KeychainService migration logic

echo "Searching for _prefixKey in git history..."
git log -p --all -S '_prefixKey' -- 'examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart' | head -50

echo -e "\nChecking KeychainService for migration logic..."
rg -n 'migration|prefix|namespace' examples/flutter/RunAnywhereAI/lib/core/services/keychain_service.dart

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate keychain files =="
ls -la examples/flutter/RunAnywhereAI/lib/core/utilities/ | cat
ls -la examples/flutter/RunAnywhereAI/lib/core/services/ | cat

echo
echo "== Current keychain_helper.dart around relevant lines =="
sed -n '1,120p' examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart

echo
echo "== Search for _prefixKey usage in Flutter code =="
rg -n "_prefixKey" examples/flutter/RunAnywhereAI/lib | head -200 || true

echo
echo "== Inspect KeychainService implementation =="
# Try to print likely file(s); if name differs, rg will catch below.
if [ -f examples/flutter/RunAnywhereAI/lib/core/services/keychain_service.dart ]; then
  sed -n '1,220p' examples/flutter/RunAnywhereAI/lib/core/services/keychain_service.dart
fi

echo
echo "== Search KeychainService for key mangling / migration / fallback =="
rg -n "class KeychainService|saveBytes|readBytes|delete|prefix|namespace|migrat|fallback" examples/flutter/RunAnywhereAI/lib/core/services | head -200 || true

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 6285


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Git work-tree status =="
git rev-parse --is-inside-work-tree && git rev-parse HEAD

echo
echo "== History search for _prefixKey (lightweight) =="
git log -n 20 --oneline -S '_prefixKey' -- examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart || true

echo
echo "== History search for 'prefixKey' (lightweight) across keychain files =="
git log -n 20 --oneline -S 'prefixKey' -- examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart examples/flutter/RunAnywhereAI/lib/core/services/keychain_service.dart || true

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files="examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart examples/flutter/RunAnywhereAI/lib/core/services/keychain_service.dart"

for c in e4212921 d4372669; do
  echo "== git show $c: _prefixKey/prefix hits =="
  git show --no-color --oneline -n 1 "$c" -- $files | rg -n "_prefixKey|prefix|namespace" | head -80 || true
  echo
done

echo "== git show $c: keychain_helper.dart excerpt (limited) =="
for c in e4212921 d4372669; do
  echo "-- $c --"
  git show --no-color "$c:examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart" | sed -n '1,140p' | rg -n "prefix|namespace|_prefixKey|saveBytes|readBytes|saveString|loadString|delete" || true
  echo
done

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 1868


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Current _prefixKey presence =="
rg -n "_prefixKey" examples/flutter/RunAnywhereAI/lib || true

echo
echo "== Search for migration/fallback patterns around keychain usage =="
rg -n "KeychainHelper|readBytes\\(|saveBytes\\(|read\\(|save\\(|delete\\(|migrat|prefix|namespace" examples/flutter/RunAnywhereAI/lib/core | head -200 || true

Repository: RunanywhereAI/runanywhere-sdks

Length of output: 2538


Breaking keychain key format change in Flutter (missing migration)

Previously KeychainHelper stored/retrieved values using a namespaced key via _prefixKey ('${_service}_$key') before calling KeychainService. Now examples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dart passes the provided key directly to KeychainService.shared (e.g., around lines 17, 25, 30, 39, 44), and KeychainService has no migration/fallback logic. Existing keychain entries saved under the old prefixed keys will become unreadable after upgrade.

  • Restore the _prefixKey behavior for backward compatibility, or
  • Add migration/fallback: when reading (and optionally deleting), try the old prefixed key first and/or copy prefixed → raw keys.
🤖 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/core/utilities/keychain_helper.dart` at
line 17, KeychainHelper stopped prefixing keys (via _prefixKey) before calling
KeychainService.shared which breaks existing entries; restore the old behavior
or add a migration/fallback: ensure write methods (e.g.,
KeychainHelper.saveBytes / saveString) call
KeychainService.shared.saveBytes/saveString with _prefixKey(key) (i.e.,
'${_service}_$key') and read methods (e.g., KeychainHelper.readBytes /
readString) first attempt to read using _prefixKey(key) and if missing try the
raw key and, on success, optionally copy the prefixed value to the raw key or
vice versa so existing prefixed entries remain readable.‬

Comment on lines +282 to +298
factory BenchmarkResult.fromJson(Map<String, dynamic> json) {
return BenchmarkResult(
id: json['id'] as String?,
timestamp: DateTime.tryParse(json['timestamp'] as String? ?? ''),
category: BenchmarkCategory.fromName(json['category'] as String? ?? '') ??
BenchmarkCategory.llm,
scenario: BenchmarkScenario.fromJson(
(json['scenario'] as Map<String, dynamic>?) ?? const {},
),
modelInfo: ComponentModelInfo.fromJson(
(json['modelInfo'] as Map<String, dynamic>?) ?? const {},
),
metrics: BenchmarkMetrics.fromJson(
(json['metrics'] as Map<String, dynamic>?) ?? const {},
),
);
}

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

Null safety violation: DateTime.tryParse can return null for non-nullable field.

Line 285 assigns DateTime.tryParse(...) to the non-nullable timestamp constructor parameter (line 257: final DateTime timestamp;). If JSON parsing fails, tryParse returns null, violating Dart's null safety and causing a runtime error.

🐛 Proposed fix to add fallback
   factory BenchmarkResult.fromJson(Map<String, dynamic> json) {
     return BenchmarkResult(
       id: json['id'] as String?,
-      timestamp: DateTime.tryParse(json['timestamp'] as String? ?? ''),
+      timestamp: DateTime.tryParse(json['timestamp'] as String? ?? '') ?? DateTime.now(),
       category: BenchmarkCategory.fromName(json['category'] as String? ?? '') ??
           BenchmarkCategory.llm,
       scenario: BenchmarkScenario.fromJson(
📝 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
factory BenchmarkResult.fromJson(Map<String, dynamic> json) {
return BenchmarkResult(
id: json['id'] as String?,
timestamp: DateTime.tryParse(json['timestamp'] as String? ?? ''),
category: BenchmarkCategory.fromName(json['category'] as String? ?? '') ??
BenchmarkCategory.llm,
scenario: BenchmarkScenario.fromJson(
(json['scenario'] as Map<String, dynamic>?) ?? const {},
),
modelInfo: ComponentModelInfo.fromJson(
(json['modelInfo'] as Map<String, dynamic>?) ?? const {},
),
metrics: BenchmarkMetrics.fromJson(
(json['metrics'] as Map<String, dynamic>?) ?? const {},
),
);
}
factory BenchmarkResult.fromJson(Map<String, dynamic> json) {
return BenchmarkResult(
id: json['id'] as String?,
timestamp: DateTime.tryParse(json['timestamp'] as String? ?? '') ?? DateTime.now(),
category: BenchmarkCategory.fromName(json['category'] as String? ?? '') ??
BenchmarkCategory.llm,
scenario: BenchmarkScenario.fromJson(
(json['scenario'] as Map<String, dynamic>?) ?? const {},
),
modelInfo: ComponentModelInfo.fromJson(
(json['modelInfo'] as Map<String, dynamic>?) ?? const {},
),
metrics: BenchmarkMetrics.fromJson(
(json['metrics'] as Map<String, dynamic>?) ?? const {},
),
);
}
🤖 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/benchmarks/benchmark_types.dart`
around lines 282 - 298, BenchmarkResult.fromJson assigns DateTime.tryParse(...)
directly to the non-nullable BenchmarkResult.timestamp; change it to use a safe
fallback when parsing fails (e.g. use the parsed value if non-null, otherwise
use a sensible default such as DateTime.now() or epoch). Update the timestamp
expression in BenchmarkResult.fromJson so it sets timestamp to
DateTime.tryParse(json['timestamp'] as String? ?? '') ?? DateTime.now() (or your
chosen default) to avoid a null assignment to the timestamp field.

Comment on lines +333 to +347
factory BenchmarkRun.fromJson(Map<String, dynamic> json) {
return BenchmarkRun(
id: json['id'] as String?,
startedAt: DateTime.tryParse(json['startedAt'] as String? ?? ''),
completedAt: DateTime.tryParse(json['completedAt'] as String? ?? ''),
results: ((json['results'] as List<dynamic>?) ?? const [])
.whereType<Map<String, dynamic>>()
.map(BenchmarkResult.fromJson)
.toList(),
status: BenchmarkRunStatus.fromName(json['status'] as String? ?? ''),
deviceInfo: BenchmarkDeviceInfo.fromJson(
(json['deviceInfo'] as Map<String, dynamic>?) ?? const {},
),
);
}

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

Null safety violation: DateTime.tryParse can return null for non-nullable field.

Line 336 assigns DateTime.tryParse(...) to the non-nullable startedAt constructor parameter (line 304: final DateTime startedAt;). If JSON parsing fails, tryParse returns null, violating Dart's null safety and causing a runtime error.

🐛 Proposed fix to add fallback
   factory BenchmarkRun.fromJson(Map<String, dynamic> json) {
     return BenchmarkRun(
       id: json['id'] as String?,
-      startedAt: DateTime.tryParse(json['startedAt'] as String? ?? ''),
+      startedAt: DateTime.tryParse(json['startedAt'] as String? ?? '') ?? DateTime.now(),
       completedAt: DateTime.tryParse(json['completedAt'] as String? ?? ''),
       results: ((json['results'] as List<dynamic>?) ?? const [])
           .whereType<Map<String, dynamic>>()
🤖 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/benchmarks/benchmark_types.dart`
around lines 333 - 347, The fromJson factory in BenchmarkRun uses
DateTime.tryParse for startedAt and completedAt which can return null but the
constructor fields startedAt and completedAt are non-nullable; update
BenchmarkRun.fromJson to provide a non-null fallback (e.g., replace
DateTime.tryParse(json['startedAt'] as String? ?? '') with
DateTime.tryParse(...) ?? DateTime.fromMillisecondsSinceEpoch(0)) or validate
and throw a clear FormatException before constructing; ensure both startedAt and
completedAt are handled and keep references to BenchmarkRun.fromJson, startedAt,
and completedAt so the change is applied to those exact fields.

Comment on lines +112 to +136
_vadSubscription = sdk.RunAnywhere.vad.streamVAD(chunks).listen(
(result) {
if (result.errorMessage.isNotEmpty) {
errorMessage = result.errorMessage;
isListening = false;
notify();
unawaited(_capture.cancel());
return;
}
isSpeech = result.isSpeech;
confidence = result.confidence;
energy = result.energy;
frameCount += 1;
notify();
},
onError: (Object e) {
errorMessage = 'VAD failed: $e';
isListening = false;
notify();
},
onDone: () {
isListening = false;
notify();
},
);

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

Incomplete cleanup on VAD stream failure.

The error paths at lines 114-119 (result.errorMessage.isNotEmpty) and lines 127-131 (onError) only update state and cancel the microphone capture, but they leave both _vadSubscription and _levelSubscription active. A failed VAD session can keep both stream listeners running after the UI indicates it has stopped.

Consolidate teardown into a single helper that cancels both subscriptions, stops recording, and updates state, then call it from all failure paths (result error, onError, and onDone).

🔒 Proposed fix to add unified teardown
+  Future<void> _handleListeningFailure(String? message) async {
+    await _vadSubscription?.cancel();
+    _vadSubscription = null;
+    await _levelSubscription?.cancel();
+    _levelSubscription = null;
+    await _capture.stopRecording();
+    if (message != null) errorMessage = message;
+    isListening = false;
+    isSpeech = false;
+    audioLevel = 0;
+    notify();
+  }
+
     _vadSubscription = sdk.RunAnywhere.vad.streamVAD(chunks).listen(
       (result) {
         if (result.errorMessage.isNotEmpty) {
-          errorMessage = result.errorMessage;
-          isListening = false;
-          notify();
-          unawaited(_capture.cancel());
+          unawaited(_handleListeningFailure(result.errorMessage));
           return;
         }
         isSpeech = result.isSpeech;
         confidence = result.confidence;
         energy = result.energy;
         frameCount += 1;
         notify();
       },
-      onError: (Object e) {
-        errorMessage = 'VAD failed: $e';
-        isListening = false;
-        notify();
-      },
-      onDone: () {
-        isListening = false;
-        notify();
-      },
+      onError: (Object e) =>
+          unawaited(_handleListeningFailure('VAD failed: $e')),
+      onDone: () => unawaited(_handleListeningFailure(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/flutter/RunAnywhereAI/lib/features/voice/vad_view_model.dart` around
lines 112 - 136, The VAD failure paths duplicate partial teardown and leave
_vadSubscription and _levelSubscription active; create a single helper method
(e.g., _teardownVAD or _stopVADSession) that 1) cancels/pauses both
_vadSubscription and _levelSubscription, 2) stops recording by awaiting or
unawaiting _capture.cancel(), and 3) sets isListening=false, clears/sets
errorMessage and calls notify(); then replace the inline cleanup in the
RunAnywhere.vad.streamVAD listener (the result.errorMessage branch), the onError
handler, and the onDone handler to call this helper so all failure/completion
paths perform the same consolidated teardown.

Comment on lines +119 to +134
_lifecycleSubscription = sdk.RunAnywhere.events.modelLifecycle.listen(
(change) {
if (change.component != component &&
change.event.category != eventCategory) {
return;
}
switch (change.kind) {
case sdk.ModelLifecycleChangeKind.loaded:
unawaited(applyCurrentModelSnapshot('loaded'));
case sdk.ModelLifecycleChangeKind.unloaded:
clearLoadedModel();
debugPrint('Voice component model unloaded');
}
},
);
}

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

Filter logic processes events from wrong components.

Lines 121-122 use && (AND) to combine the component and category mismatch checks:

if (change.component != component &&
    change.event.category != eventCategory) {
  return;
}

This skips events only when both component and category don't match, which means it will process events if either the component matches or the category matches. A VAD ViewModel could process STT events if the categories happen to align, leading to incorrect state updates.

Change the operator to || (OR) so events are skipped when either the component or category doesn't match, ensuring only events for this specific component are processed.

🐛 Proposed fix
     _lifecycleSubscription = sdk.RunAnywhere.events.modelLifecycle.listen(
       (change) {
-        if (change.component != component &&
+        if (change.component != component ||
             change.event.category != eventCategory) {
           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/flutter/RunAnywhereAI/lib/features/voice/voice_component_view_model_base.dart`
around lines 119 - 134, The event filter in the _lifecycleSubscription callback
for sdk.RunAnywhere.events.modelLifecycle.listen is using && so it only returns
when both change.component != component and change.event.category !=
eventCategory; change the condition to use || so the callback returns (skips the
event) if either the component or the eventCategory doesn't match. Update the
check referencing change.component, component, change.event.category and
eventCategory accordingly to ensure only events for this specific
component/category are processed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant