Cleanup V2 transition + testing + release#504
Conversation
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>
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
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.
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() |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 009017d. Configure here.
There was a problem hiding this comment.
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}%") | ||
| } |
There was a problem hiding this comment.
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.
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) |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 009017d. Configure here.
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (49)
📝 WalkthroughWalkthroughThe 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. ChangesExample apps and SDK runtime changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winFail the benchmark when the LLM run times out.
withTimeoutOrNullreturnsnullhere, but the function still returns metrics andrun()records the scenario assuccess = 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 winPin the Silero artifact to an immutable URL.
size_bytesnow matches the currentraw/masterfile 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 winWait for Phase 2 to stop before tearing down the bridge.
servicesInitJob?.cancel()only requests cancellation.completeServicesInitialization()can still be insideinitializePlatformBridgeServices()orCppBridgeSdkInit.phase2()whenshutdownPlatformBridge()/CppBridgeState.shutdown()run, soreset()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 winRace condition on
isRunningguard.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 == falsebefore either sets it totrue, 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 winUse 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 winWrap tool parameters instead of forcing one horizontal row.
This
Rowwill 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 asFlowRowhere 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 winConsider validating the
sampleRateparameter.Since
pcm16ToWavis 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 incorrectbyteRateand 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 winConsider validating the
sampleRateparameter.Since
pcm16ToWavis 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 incorrectbyteRateand 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
InConversationHistorySheet.kt(lines 63–66),ConversationRepository.search(query)runs on every recomposition while typing; howeverConversationRepository.search()is synchronous and only filters the in-memoryconversations/summaries(noFile/readText/Dispatchers.IO). Disk/JSON IO happens inConversationStoreinside the suspendrefresh()/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 winCross-SDK consistency: Kotlin lacks this sherpa registration guard.
The Swift
HybridSTTRouternow 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(insdk/runanywhere-kotlin/.../HybridSTTRouter.kt) has no equivalent guard, so Kotlin users who forgetONNX.register()will encounter a less helpful vtable lookup failure deeper in the stack.Consider adding the same guard to Kotlin's
HybridRouterBridgeAdapter.createServicefor 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
📒 Files selected for processing (61)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelBootstrap.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelCatalog.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/benchmark/BenchmarkRunner.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/conversation/ConversationModels.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/conversation/ConversationRepository.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/settings/AppSettings.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/settings/SettingsRepository.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/state/GlobalState.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/navigation/AppNavHost.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/navigation/Destinations.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatDetailsSheet.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatModels.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatTopBar.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ConversationHistorySheet.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/intro/IntroScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelSelectionContext.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/more/MoreScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/stt/SttViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/system_ui/AppScaffold.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/system_ui/AppTopBar.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tools/ToolsScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tools/ToolsViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vad/VadScreen.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vad/VadViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/icons/RACIcons.ktexamples/ios/RunAnywhereAI/RunAnywhereAI/Core/Services/ModelCatalogBootstrap.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+Analytics.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+Generation.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/VoiceAgentViewModel.swiftsdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cppsdk/runanywhere-kotlin/build.gradle.ktssdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/HTTPClientAdapter.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeAuth.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeState.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/httptransport/OkHttpHttpTransport.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/HybridSTTRouter.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/RunAnywhere.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Audio/RunAnywhereAudioConvert.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereLoRA.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereStructuredOutput.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereTextGeneration.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/RunAnywhereToolCalling.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/StructuredOutputProtoHelpers.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/ToolCallingOrchestrator.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Models/RunAnywhereModelRegistry.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/RAG/RunAnywhereRAG.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereDownload.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/RunAnywhereStorage.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VAD/RunAnywhereVAD.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VoiceAgent/RunAnywhereVoiceAgent.ktsdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Public/Events/EventBus.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/Audio/RAAudioConvert.swift
| fun retrySdkSetup() { | ||
| GlobalState.clearInitError() | ||
| appScope.launch(Dispatchers.IO) { | ||
| runSdkSetup() | ||
| } |
There was a problem hiding this comment.
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.
| fun markInitFailed(message: String) { | ||
| initError = message | ||
| } |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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`.
| suspend fun RunAnywhere.ragGetDocumentCount(): Int = | ||
| withContext(Dispatchers.IO) { | ||
| ensureServicesReady() | ||
| CppBridgeRAG.stats().indexed_chunks.toInt() | ||
| try { | ||
| CppBridgeRAG.stats().indexed_chunks.toInt() | ||
| } catch (_: Exception) { | ||
| 0 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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, | ||
| ) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| 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, | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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()) | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 winTool-assisted generations lack analytics.
generateWithToolsdoesn't populateGenerationStats, unlikegenerateReplyandstreamReply. This means tool-calling messages won't have token counts, latency, or throughput metrics persisted or displayed inChatDetailsSheet.Consider extracting stats from
result(it likely hastokens_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 valueMissing UI control for
hybridMinBatterysetting.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
⛔ Files ignored due to path filters (8)
sdk/runanywhere-commons/src/generated/proto/rag.pb.ccis excluded by!**/generated/**sdk/runanywhere-commons/src/generated/proto/rag.pb.his excluded by!**/generated/**sdk/runanywhere-flutter/packages/runanywhere/lib/generated/convenience/ra_convenience.dartis excluded by!**/generated/**sdk/runanywhere-flutter/packages/runanywhere/lib/generated/rag.pb.dartis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/RAGConfiguration.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/convenience/RAConvenience.ktis excluded by!**/generated/**sdk/runanywhere-swift/Sources/RunAnywhere/Generated/RAConvenience.swiftis excluded by!**/generated/**sdk/runanywhere-swift/Sources/RunAnywhere/Generated/rag.pb.swiftis excluded by!**/generated/**
📒 Files selected for processing (35)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/lora/LoraViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/solutions/SolutionsYaml.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/tts/TtsViewModel.ktexamples/android/RunAnywhereAI/scripts/sync-solutions-yamls.shexamples/ios/RunAnywhereAI/RunAnywhereAI/App/RunAnywhereAIApp.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/STTViewModel.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Voice/SpeechToTextView.swiftidl/codegen/generate_swift_convenience.pyidl/rag.protosdk/runanywhere-commons/src/features/rag/rac_rag_proto_abi.cppsdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cppsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/CppBridge.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeSDKEvents.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeStructuredOutput.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeTelemetry.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/errors/SDKException.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/Cloud.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/HybridRoutingPolicy.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/infrastructure/logging/SDKLogger.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/RunAnywhere.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/events/EventBus.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/LLM/ToolCallingTypes.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Models/ModelTypesArtifacts.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Models/RunAnywhereModelLifecycle.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/Storage/DownloadStageExtensions.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VoiceAgent/VoiceAgentTypes.ktsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/CppBridge.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Telemetry.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/AppleDeviceStateProvider.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Public/Extensions/RAG/RAGProto+Helpers.swiftsdk/shared/proto-ts/src/convenience/rag_convenience.tssdk/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
| 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() } |
There was a problem hiding this comment.
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) != nullOr 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.
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 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 -60Repository: 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.swiftRepository: 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.swiftRepository: 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.
| /** 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, | ||
| ) |
There was a problem hiding this comment.
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.
| /** 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.
| 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 |
There was a problem hiding this comment.
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.
| 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)") |
There was a problem hiding this comment.
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
| if (m.chunkOverlap !== undefined && (m.chunkOverlap < 0)) { | ||
| throw new ValidationError({ | ||
| fieldPath: 'RAGConfiguration.chunk_overlap', | ||
| message: `chunk_overlap must be in >= 0 (got ${m.chunkOverlap})`, | ||
| }); | ||
| } |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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 winDocstring appears misplaced after method insertion.
The docstring on lines 391-393 starts with "Destroy a router handle…" but is attached to
cancelRouter. This looks likecancelRouterwas inserted mid-docstring. ThedestroyRoutermethod 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 winVariable shadowing:
modelparameter overwritten with registry fetch.The function accepts
model: ModelInfoas 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 valueDuplicate
-DRCT_REMOVE_LEGACY_ARCH=1flag in C++ compiler flags.
OTHER_CPLUSPLUSFLAGSincludes the flag twice:
- Inherited via
$(OTHER_CFLAGS)(line 610)- 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 valueDuplicate
-DRCT_REMOVE_LEGACY_ARCH=1flag in C++ compiler flags (Release).Same redundancy as Debug configuration:
OTHER_CPLUSPLUSFLAGSincludes 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 valueInconsistent input validation between
registerProviderandunregisterProvider.
registerProviderthrowsSDKException.invalidInputfor an empty name (line 254-256), butunregisterProvidersilently 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 valueConsider using a download-specific exception type.
SDKException.invalidConfigurationis 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 valueMemory may not be freed on all code paths, or freed when uninitialized.
The
rac_proto_buffer_t out{}is zero-initialized, soout.dataandout.error_messagestart asnullptr. However:
- If
fn(rc, &out)fails,out.error_messagemight contain an error string that should be freed- 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 valueUnused typedef after
registerTypedToolremoval.
TypedToolExecutoris no longer used in this file sinceregisterTypedToolwas 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 valueConsider a more accurate error category.
The error is thrown when download succeeded but
localPathwas not persisted. This is a storage/registry issue, not a network issue.ERROR_CATEGORY_STORAGEorERROR_CATEGORY_INTERNALwould be more precise thanERROR_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
⛔ Files ignored due to path filters (2)
sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.hppis excluded by!**/generated/**
📒 Files selected for processing (63)
examples/flutter/RunAnywhereAI/lib/core/services/audio_player_service.dartexamples/flutter/RunAnywhereAI/lib/core/services/audio_recording_service.dartexamples/flutter/RunAnywhereAI/lib/features/voice/speech_to_text_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/text_to_speech_view.dartexamples/flutter/RunAnywhereAI/pubspec.yamlexamples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxprojexamples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsxexamples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsxsdk/runanywhere-flutter/AGENTS.mdsdk/runanywhere-flutter/packages/runanywhere/lib/features/stt/services/audio_capture_manager.dartsdk/runanywhere-flutter/packages/runanywhere/lib/features/tts/services/audio_playback_manager.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_audio.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_hybrid_stt.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_lora.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_models.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_stt.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tools.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tts.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vad.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vlm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_voice.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/events/event_bus.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/audio/audio_convert.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/hybrid/hybrid_stt_router.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dartsdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere.dartsdk/runanywhere-flutter/packages/runanywhere/pubspec.yamlsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Hybrid.cppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.cppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.hppsdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cppsdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.hppsdk/runanywhere-react-native/packages/core/src/Adapters/HandleStreamAdapter.tssdk/runanywhere-react-native/packages/core/src/Adapters/LLMStreamAdapter.tssdk/runanywhere-react-native/packages/core/src/Adapters/VoiceAgentStreamAdapter.tssdk/runanywhere-react-native/packages/core/src/Foundation/Constants/SDKConstants.tssdk/runanywhere-react-native/packages/core/src/Foundation/Errors/SDKException.tssdk/runanywhere-react-native/packages/core/src/Foundation/Errors/index.tssdk/runanywhere-react-native/packages/core/src/Foundation/Initialization/InitializationState.tssdk/runanywhere-react-native/packages/core/src/Foundation/Initialization/ServicesReadyGuard.tssdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Logger/SDKLogger.tssdk/runanywhere-react-native/packages/core/src/Foundation/Logging/Services/LoggingManager.tssdk/runanywhere-react-native/packages/core/src/Public/Events/EventBus.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Audio/RunAnywhere+AudioConvert.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Embeddings/RunAnywhere+Embeddings.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/CloudSTT.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/HybridSTTRouter.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/index.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+StructuredOutput.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+ToolCalling.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelLifecycle.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/RAG/RunAnywhere+RAG.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Solutions/RunAnywhere+Solutions.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/RunAnywhere+Storage.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/RunAnywhere+VisionLanguage.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/VoiceAgent/RunAnywhere+VoiceAgent.tssdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.tssdk/runanywhere-react-native/packages/core/src/index.tssdk/runanywhere-react-native/packages/core/src/specs/RunAnywhereCore.nitro.tssdk/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
| final urlTail = entry.url.split('/').isNotEmpty | ||
| ? entry.url.split('/').last | ||
| : entry.url; |
There was a problem hiding this comment.
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.
| 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.
| if (wavData != null && wavData.isNotEmpty) { | ||
| await _playback.play(wavData); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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; |
There was a problem hiding this comment.
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.
| 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.
| 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; |
There was a problem hiding this comment.
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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
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 winCancel 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_sessionkeeps a livesessionId/callback/handle afterrac_stt_component_cleanup(handle), so later callbacks orsttStreamStop()/sttStreamCancel()operate on torn-down native state. Reuse the sametakeSTTStreamSession()→cancelSTTStreamSession()→g_stt_stream_loaded_model_id.clear()sequence thatresetAllGlobalComponentHandles()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 liftRestore the required platform-specific TTS implementations on this screen.
This change routes both iOS and Android through
RunAnywhere.speak()only. ForTTSScreen, the repo contract is the opposite: iOS should useNativeModules.NativeAudioModuleand Android should lazy-loadreact-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 valueUse bitwise check for interruption options flags.
The
flagsparameter is anOptionSetraw value that could contain multiple bits. Using exact equality works today (only.shouldResumeexists), 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 valueCatch-all exception handling may mask unexpected errors.
The
catch (_)swallows all exceptions fromensureServicesReady(), treating them uniformly as "not ready" states. IfensureServicesReady()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 valueIncorrect error category for missing local path.
The error category
ERROR_CATEGORY_NETWORKis semantically incorrect for a missinglocal_pathcondition. This is a data/state issue, not a network issue. Consider usingERROR_CATEGORY_INTERNALorERROR_CATEGORY_STORAGEif 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 valueSame category issue for import failure.
The
ERROR_CATEGORY_NETWORKis 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
⛔ Files ignored due to path filters (31)
sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JHybridAudioCaptureSpec.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JHybridAudioCaptureSpec.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JHybridAudioPlaybackSpec.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/c++/JHybridAudioPlaybackSpec.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/kotlin/com/margelo/nitro/runanywhere/Func_void_std__shared_ptr_ArrayBuffer_.ktis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/kotlin/com/margelo/nitro/runanywhere/HybridAudioCaptureSpec.ktis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/kotlin/com/margelo/nitro/runanywhere/HybridAudioPlaybackSpec.ktis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/runanywherecore+autolinking.cmakeis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/android/runanywherecoreOnLoad.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCore-Swift-Cxx-Bridge.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCore-Swift-Cxx-Bridge.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCore-Swift-Cxx-Umbrella.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCoreAutolinking.mmis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/RunAnywhereCoreAutolinking.swiftis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/c++/HybridAudioCaptureSpecSwift.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/c++/HybridAudioCaptureSpecSwift.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/c++/HybridAudioPlaybackSpecSwift.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/c++/HybridAudioPlaybackSpecSwift.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/Func_void.swiftis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swiftis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/HybridAudioCaptureSpec.swiftis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/HybridAudioCaptureSpec_cxx.swiftis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/HybridAudioPlaybackSpec.swiftis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/ios/swift/HybridAudioPlaybackSpec_cxx.swiftis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridAudioCaptureSpec.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridAudioCaptureSpec.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridAudioPlaybackSpec.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridAudioPlaybackSpec.hppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.hppis excluded by!**/generated/**
📒 Files selected for processing (105)
examples/react-native/RunAnywhereAI/ios/RunAnywhereAI.xcodeproj/project.pbxprojexamples/react-native/RunAnywhereAI/ios/RunAnywhereAI/NativeAudioModule.mexamples/react-native/RunAnywhereAI/ios/RunAnywhereAI/NativeAudioModule.swiftexamples/react-native/RunAnywhereAI/knip.jsonexamples/react-native/RunAnywhereAI/package.jsonexamples/react-native/RunAnywhereAI/react-native.config.jsexamples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsxexamples/react-native/RunAnywhereAI/src/screens/STTScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsxexamples/react-native/RunAnywhereAI/src/utils/modelDisplay.tssdk/runanywhere-commons/include/rac/infrastructure/events/rac_sdk_event_stream.hsdk/runanywhere-commons/src/core/rac_core.cppsdk/runanywhere-commons/src/core/sdk_state.cppsdk/runanywhere-commons/src/infrastructure/events/event_publisher.cppsdk/runanywhere-commons/src/lifecycle/sdk_init.cppsdk/runanywhere-commons/src/router/hybrid/rac_stt_hybrid_router_proto.cppsdk/runanywhere-commons/tests/test_sdk_event_stream.cppsdk/runanywhere-flutter/packages/runanywhere/lib/core/module/runanywhere_module.dartsdk/runanywhere-flutter/packages/runanywhere/lib/core/native/rac_native.dartsdk/runanywhere-flutter/packages/runanywhere/lib/foundation/constants/sdk_constants.dartsdk/runanywhere-flutter/packages/runanywhere/lib/foundation/logging/sdk_logger.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_llm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_model_registry.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_tts.dartsdk/runanywhere-flutter/packages/runanywhere/lib/native/type_conversions/model_types_cpp_bridge.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_downloads.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_llm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_lora.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_models.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_plugin_loader.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_stt.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tools.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tts.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vad.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vlm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_voice.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/configuration/sdk_environment.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/model_category_extensions.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/rag_module.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_logging.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_structured_output.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/hybrid/hybrid_stt_router.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dartsdk/runanywhere-flutter/packages/runanywhere_genie/lib/genie.dartsdk/runanywhere-flutter/packages/runanywhere_llamacpp/lib/llamacpp.dartsdk/runanywhere-flutter/packages/runanywhere_onnx/lib/onnx.dartsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/hybrid/HybridSTTRouter.ktsdk/runanywhere-react-native/packages/core/README.mdsdk/runanywhere-react-native/packages/core/android/src/main/AndroidManifest.xmlsdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/HybridAudioCapture.ktsdk/runanywhere-react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/HybridAudioPlayback.ktsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+ProtoCompat.hppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Registry.cppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Storage.cppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Voice.cppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.hppsdk/runanywhere-react-native/packages/core/cpp/HybridVoiceAgent.cppsdk/runanywhere-react-native/packages/core/cpp/bridges/InitBridge.cppsdk/runanywhere-react-native/packages/core/ios/HybridAudioCapture.swiftsdk/runanywhere-react-native/packages/core/ios/HybridAudioPlayback.swiftsdk/runanywhere-react-native/packages/core/nitro.jsonsdk/runanywhere-react-native/packages/core/src/Features/VoiceSession/AudioCaptureManager.tssdk/runanywhere-react-native/packages/core/src/Features/VoiceSession/AudioPlaybackManager.tssdk/runanywhere-react-native/packages/core/src/Foundation/Errors/SDKException.tssdk/runanywhere-react-native/packages/core/src/Foundation/Initialization/InitializedGuard.tssdk/runanywhere-react-native/packages/core/src/Foundation/Initialization/index.tssdk/runanywhere-react-native/packages/core/src/Internal/Nitro/NitroAudioCaptureSpec.tssdk/runanywhere-react-native/packages/core/src/Internal/Nitro/NitroAudioPlaybackSpec.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Embeddings/EmbeddingsProto+Helpers.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Embeddings/RunAnywhere+Embeddings.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/HybridDeviceState.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Hybrid/HybridSTTRouter.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+LoRA.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+StructuredOutput.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+TextGeneration.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+ToolCalling.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelLifecycle.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Models/RunAnywhere+ModelRegistry.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/RAG/RunAnywhere+RAG.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/STT/RunAnywhere+STT.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Solutions/RunAnywhere+Solutions.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/RunAnywhere+Storage.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/Storage/StorageProto+Helpers.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/TTS/RunAnywhere+TTS.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/VAD/RunAnywhere+VAD.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/RunAnywhere+VisionLanguage.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/VLM/VLMImage+Helpers.tssdk/runanywhere-react-native/packages/core/src/Public/Extensions/VoiceAgent/RunAnywhere+VoiceAgent.tssdk/runanywhere-react-native/packages/core/src/Public/Helpers/SDKComponent+DisplayName.tssdk/runanywhere-react-native/packages/core/src/Public/Helpers/SDKEnvironment+Helpers.tssdk/runanywhere-react-native/packages/core/src/Public/Helpers/formatFramework.tssdk/runanywhere-react-native/packages/core/src/Public/RunAnywhere.tssdk/runanywhere-react-native/packages/core/src/index.tssdk/runanywhere-react-native/packages/core/src/specs/AudioCapture.nitro.tssdk/runanywhere-react-native/packages/core/src/specs/AudioPlayback.nitro.tssdk/runanywhere-react-native/packages/core/src/specs/RunAnywhereCore.nitro.tssdk/runanywhere-react-native/packages/onnx/src/ONNX.tssdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Telemetry.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Constants/SDKConstants.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Security/KeychainManager.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Hybrid/HybridSTTRouter.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Public/RunAnywhere.swiftthoughts/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'; |
There was a problem hiding this comment.
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
| 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())); | ||
| } |
There was a problem hiding this comment.
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()).
| export 'package:runanywhere/foundation/logging/sdk_logger.dart' | ||
| show | ||
| ConsoleLogDestination, | ||
| LogDestination, | ||
| LoggingConfigurations, | ||
| SDKLoggerConfig; | ||
| export 'package:runanywhere/generated/logging.pb.dart' | ||
| show LogEntry, LoggingConfiguration; |
There was a problem hiding this comment.
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.
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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; |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
🧩 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"
doneRepository: 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
doneRepository: 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.
There was a problem hiding this comment.
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 winRemove
console.logstatement per coding guidelines.Same issue — only
console.warnandconsole.errorare 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 winMissing EOS token stripping per coding guidelines.
The coding guidelines specify: "Strip EOS tokens from VLM model output in VLMService before returning results." The refactored
processImagestreams tokens directly viaonToken(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 winConsider error handling for navigation failures.
The
_pushhelper usesunawaited(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
📒 Files selected for processing (41)
examples/flutter/RunAnywhereAI/lib/app/content_view.dartexamples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/constants.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_status_components.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_types.dartexamples/flutter/RunAnywhereAI/lib/features/more/more_view.dartexamples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dartexamples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dartexamples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dartexamples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dartexamples/react-native/RunAnywhereAI/App.tsxexamples/react-native/RunAnywhereAI/README.mdexamples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsxexamples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsxexamples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsxexamples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.tsexamples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsxexamples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/STTScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VADScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsxexamples/react-native/RunAnywhereAI/src/services/VLMService.tsexamples/react-native/RunAnywhereAI/src/types/index.tsexamples/react-native/RunAnywhereAI/src/types/settings.tsexamples/react-native/RunAnywhereAI/src/utils/loraFixture.tsexamples/react-native/RunAnywhereAI/src/utils/modelDisplay.tssdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dartsdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+TextGeneration.tsthoughts/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
| set toolCallingEnabled(bool value) { | ||
| _toolCallingEnabled = value; | ||
| unawaited(_saveSettings()); | ||
| unawaited(_saveSettingsAndSyncTools()); | ||
| notifyListeners(); | ||
| } |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
| const ITEMS: MoreItem[] = [ | ||
| { | ||
| route: 'STT', | ||
| title: 'Transcribe', | ||
| subtitle: 'Speech-to-text', | ||
| icon: 'pulse-outline', | ||
| }, |
There was a problem hiding this comment.
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.
| 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'.
- 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>
There was a problem hiding this comment.
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 winTool calling state may not reflect actual registration status on failure.
Lines 349-350 set
toolCallingEnabledtotrueand persist it toAsyncStorageimmediately, before the asyncregisterSharedDemoTools()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
setToolCallingEnabledandAsyncStorage.setIteminside 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 winAdd timeouts around the Open-Meteo calls.
Both
http.get()calls can hang indefinitely. When this tool is invoked fromgenerateWithTools(), 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 winSelection never enters a loading state.
isLoadingModelandselectedModelIdare only consumed by the UI; they are never set before awaitingonModelSelected(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 winDon'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 | 🟠 MajorDon’t allow
_clearChat()to run during_generateStreaming()token updates (and tool-format selection is already correct)
chat_interface_view.dart’s_generateStreaming()capturesmessageIndexand updates_messages[messageIndex]inside the streamingonTokencallback with no bounds guard._clearChat()(delete button and “new chat” from conversation history) clears_messagesvia_messages.clear()while generation may still be in progress, so the capturedmessageIndexcan become invalid, causing out-of-range updates / writing into the wrong placeholder row.- The LFM2 tool-call selection in
_detectToolCallFormat()returningToolCallFormatName.TOOL_CALL_FORMAT_NAME_PYTHONICforlfm2+toolmatches the SDK/commons mapping:TOOL_CALL_FORMAT_NAME_PYTHONICresolves to the"lfm2"hint (rac_tool_call_format_hint_from_format_name→"lfm2"viaresolvedFormatName).🤖 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 winRemove console.log per coding guidelines.
This was flagged in a previous review. The coding guidelines specify that only
console.warnandconsole.errorare allowed in React Native code. Replace withconsole.warnor 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 liftThis bypasses the required platform-specific STT capture paths.
STTScreennow hard-wiresAudioCaptureManagerfor both platforms. According to coding guidelines, this sample is supposed to use the iOS AVFoundation path viaNativeAudioModule.startRecording()and the Android native recorder path withRunAnywhere.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 winEOS 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,
VLMServiceshould "Strip EOS tokens from VLM model output... before returning results." Review ofVLMService.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(). InVLMService.ts, add a helper to strip common EOS tokens and apply it before callingonToken:// 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 tofalse(key-missing path)Both
ChatScreenandSettingsScreenderivethinkingModeEnabledasthinkingStr === 'true'(so whenTHINKING_MODE_ENABLEDis missing/null, it resolves tofalse), andSettingsScreeninitializes the toggle state withuseState(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 valueConsider using a sentinel value or proper typing for done state.
The manual async iterator implementation uses
undefined as unknown as Uint8Arrayin 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
📒 Files selected for processing (49)
examples/flutter/RunAnywhereAI/lib/app/content_view.dartexamples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/constants.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_status_components.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_types.dartexamples/flutter/RunAnywhereAI/lib/features/more/more_view.dartexamples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dartexamples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dartexamples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dartexamples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dartexamples/react-native/RunAnywhereAI/App.tsxexamples/react-native/RunAnywhereAI/README.mdexamples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsxexamples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsxexamples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsxexamples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.tsexamples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsxexamples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/STTScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VADScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsxexamples/react-native/RunAnywhereAI/src/services/VLMService.tsexamples/react-native/RunAnywhereAI/src/types/index.tsexamples/react-native/RunAnywhereAI/src/types/settings.tsexamples/react-native/RunAnywhereAI/src/utils/loraFixture.tsexamples/react-native/RunAnywhereAI/src/utils/modelDisplay.tsexamples/web/RunAnywhereAI/src/components/model-selection.tsexamples/web/RunAnywhereAI/src/main.tsexamples/web/RunAnywhereAI/src/services/model-catalog.tsexamples/web/RunAnywhereAI/src/views/solutions.tsexamples/web/RunAnywhereAI/src/views/vision.tssdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dartsdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+TextGeneration.tssdk/runanywhere-web/packages/core/src/Foundation/EventBus.tssdk/runanywhere-web/packages/core/src/Foundation/RACErrors.tssdk/runanywhere-web/packages/core/src/Foundation/SDKException.tssdk/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
| case 'vad': | ||
| return 'pulse-outline'; |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| export const DEFAULT_INFERENCE_FRAMEWORK = | ||
| InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP; | ||
| export const SYSTEM_TTS_FRAMEWORK = | ||
| InferenceFramework.INFERENCE_FRAMEWORK_SYSTEM_TTS; | ||
| InferenceFramework.INFERENCE_FRAMEWORK_UNSPECIFIED; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 winDon't keep async assistant updates keyed to a stale list index.
Both the tool-calling and streaming paths capture
messageIndexonce, but_clearChat()can empty_messageswhile generation is still running. The next token/final update then writes through_messages[messageIndex]and throws a reachableRangeError.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 winRemove disallowed
console.logdiagnostics (two sites).
console.logis 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 | 🟠 MajorMake
startListeningrollback-safe and reentrant-safe.This still publishes
streamRef,taskRef, andisListeningRefbeforestartRecording()succeeds. If mic startup throws, or the button is tapped again beforesetIsListening(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 winUnify internal imports to tsconfig aliases across
MoreScreen.tsx,StorageScreen.tsx, andTabNavigator.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 liftMove storage/data orchestration into a
ChangeNotifierview model.This screen currently keeps feature-level storage/model-fetch/action state in
StatefulWidget. Per project pattern, that logic should live in a singletonChangeNotifierconsumed withListenableBuilder, whilesetStatestays 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
📒 Files selected for processing (49)
examples/flutter/RunAnywhereAI/lib/app/content_view.dartexamples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/constants.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_status_components.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_types.dartexamples/flutter/RunAnywhereAI/lib/features/more/more_view.dartexamples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dartexamples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dartexamples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dartexamples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dartexamples/react-native/RunAnywhereAI/App.tsxexamples/react-native/RunAnywhereAI/README.mdexamples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsxexamples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsxexamples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsxexamples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.tsexamples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsxexamples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/STTScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VADScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsxexamples/react-native/RunAnywhereAI/src/services/VLMService.tsexamples/react-native/RunAnywhereAI/src/types/index.tsexamples/react-native/RunAnywhereAI/src/types/settings.tsexamples/react-native/RunAnywhereAI/src/utils/loraFixture.tsexamples/react-native/RunAnywhereAI/src/utils/modelDisplay.tsexamples/web/RunAnywhereAI/src/components/model-selection.tsexamples/web/RunAnywhereAI/src/main.tsexamples/web/RunAnywhereAI/src/services/model-catalog.tsexamples/web/RunAnywhereAI/src/views/solutions.tsexamples/web/RunAnywhereAI/src/views/vision.tssdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dartsdk/runanywhere-react-native/packages/core/src/Public/Extensions/LLM/RunAnywhere+TextGeneration.tssdk/runanywhere-web/packages/core/src/Foundation/EventBus.tssdk/runanywhere-web/packages/core/src/Foundation/RACErrors.tssdk/runanywhere-web/packages/core/src/Foundation/SDKException.tssdk/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 |
There was a problem hiding this comment.
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
| try { | ||
| debugPrint('🎯 Initializing SDK...'); | ||
|
|
||
| await _registerBackends(); | ||
|
|
There was a problem hiding this comment.
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
| if (Genie.isAvailable) { | ||
| await Genie.register(priority: 200); | ||
| debugPrint( | ||
| '✅ Genie backend registered; NPU model catalog is pending generated registry/catalog support', | ||
| ); |
There was a problem hiding this comment.
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
| _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); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
| 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)); | ||
| } | ||
| ); |
There was a problem hiding this comment.
🧩 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" || trueRepository: 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
fiRepository: 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
fiRepository: 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
fiRepository: 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
| const handleStopGeneration = useCallback(() => { | ||
| generationAbortRef.current?.abort(); | ||
| void RunAnywhere.cancelGeneration(); | ||
| setIsLoading(false); | ||
| }, []); |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🧩 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"
doneRepository: 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
fiRepository: 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.tsxRepository: RunanywhereAI/runanywhere-sdks
Length of output: 2005
Handle SDK failures in storage actions and fix tsconfig path aliases in StorageScreen.tsx
refreshusestry/finallywithout acatch, and it’s invoked viavoid refresh()inuseEffect, 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 fromtsconfig.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.
| 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.
| 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?.(); | ||
| } |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (5)
examples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dart (1)
76-78: ⚡ Quick winRemove redundant
.toInt()call.The
clamp()method onintalready returns anint, 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.durationMsin the SDK is already anInt64millisecond value, soresult.durationMs.toInt()isn’t a double→int precision-loss issue.- For the default
tts.synthesize()path, the SDK setsaudioFormattoAUDIO_FORMAT_PCM, and the SDK’sspeak()convertsTTSOutput.audioDatausingrac_audio_float32_to_wav(Float32 PCM), which aligns with the “/4 bytes per sample” fallback.- Robustness: if
audioFormatcan vary (e.g.,AUDIO_FORMAT_PCM_S16LE), prefer usingresult.audioFormatto 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 valueReplace
console.logwithconsole.warnfor diagnostic messages.Per coding guidelines, only
console.warnandconsole.errorare allowed. While the eslint-disable comment acknowledges this for "demo app bootstrap diagnostics", consider usingconsole.warnor thelogDiagnosticutility (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 valueConsider 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 valueConsider 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
⛔ Files ignored due to path filters (3)
examples/flutter/RunAnywhereAI/lib/generated/solutions_yaml.dartis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.cppis excluded by!**/generated/**sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.hppis excluded by!**/generated/**
📒 Files selected for processing (104)
examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/vision/VisionViewModel.ktexamples/flutter/RunAnywhereAI/README.mdexamples/flutter/RunAnywhereAI/lib/app/content_view.dartexamples/flutter/RunAnywhereAI/lib/app/runanywhere_ai_app.dartexamples/flutter/RunAnywhereAI/lib/core/services/conversation_store.dartexamples/flutter/RunAnywhereAI/lib/core/services/model_catalog_bootstrap.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/constants.dartexamples/flutter/RunAnywhereAI/lib/core/utilities/keychain_helper.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_dashboard_view.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_runner.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_store.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_types.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/benchmark_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/llm_benchmark_provider.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/stt_benchmark_provider.dartexamples/flutter/RunAnywhereAI/lib/features/benchmarks/tts_benchmark_provider.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_lora_sheet.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_list_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_status_components.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_types.dartexamples/flutter/RunAnywhereAI/lib/features/more/more_view.dartexamples/flutter/RunAnywhereAI/lib/features/rag/rag_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/settings/combined_settings_view.dartexamples/flutter/RunAnywhereAI/lib/features/settings/storage_view.dartexamples/flutter/RunAnywhereAI/lib/features/settings/tool_settings_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/solutions/solutions_view.dartexamples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dartexamples/flutter/RunAnywhereAI/lib/features/tools/tools_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/speech_to_text_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/stt_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/voice/text_to_speech_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/tts_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/voice/vad_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/vad_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_agent_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_assistant_view.dartexamples/flutter/RunAnywhereAI/lib/features/voice/voice_component_view_model_base.dartexamples/flutter/RunAnywhereAI/pubspec.yamlexamples/flutter/RunAnywhereAI/scripts/sync-solutions-yamls.shexamples/flutter/RunAnywhereAI/scripts/verify.shexamples/ios/RunAnywhereAI/RunAnywhereAI/Core/Services/ModelCatalogBootstrap.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Vision/VLMViewModel.swiftexamples/react-native/RunAnywhereAI/App.tsxexamples/react-native/RunAnywhereAI/README.mdexamples/react-native/RunAnywhereAI/src/components/chat/ChatInput.tsxexamples/react-native/RunAnywhereAI/src/components/chat/LoRASheet.tsxexamples/react-native/RunAnywhereAI/src/components/chat/index.tsexamples/react-native/RunAnywhereAI/src/components/common/ModelRequiredOverlay.tsxexamples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsxexamples/react-native/RunAnywhereAI/src/hooks/useVLMCamera.tsexamples/react-native/RunAnywhereAI/src/navigation/TabNavigator.tsxexamples/react-native/RunAnywhereAI/src/screens/BenchmarkScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/MoreScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/RAGScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/STTScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/SettingsScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/StorageScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/TTSScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VADScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/ValidationHarnessScreen.tsxexamples/react-native/RunAnywhereAI/src/screens/VoiceAssistantScreen.tsxexamples/react-native/RunAnywhereAI/src/services/ModelCatalogBootstrap.tsexamples/react-native/RunAnywhereAI/src/services/VLMService.tsexamples/react-native/RunAnywhereAI/src/types/index.tsexamples/react-native/RunAnywhereAI/src/types/settings.tsexamples/react-native/RunAnywhereAI/src/utils/chatSampleTools.tsexamples/react-native/RunAnywhereAI/src/utils/loraFixture.tsexamples/react-native/RunAnywhereAI/src/utils/modelDisplay.tsexamples/react-native/RunAnywhereAI/src/utils/syntheticAudio.tsexamples/web/RunAnywhereAI/src/components/model-selection.tsexamples/web/RunAnywhereAI/src/main.tsexamples/web/RunAnywhereAI/src/services/model-catalog.tsexamples/web/RunAnywhereAI/src/views/solutions.tsexamples/web/RunAnywhereAI/src/views/vision.tsidl/codegen/swift-modality-abi.yamlsdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_component.hsdk/runanywhere-commons/include/rac/features/vlm/rac_vlm_service.hsdk/runanywhere-commons/src/core/model_lifecycle.cppsdk/runanywhere-commons/src/core/model_lifecycle_resolve.cppsdk/runanywhere-commons/src/features/common/rac_service_factory_internal.hsdk/runanywhere-commons/src/features/vlm/vlm_module.cppsdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cppsdk/runanywhere-commons/tests/test_advanced_modality_proto_abi.cppsdk/runanywhere-flutter/packages/runanywhere/lib/foundation/constants/sdk_constants.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_llm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_tools.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/capabilities/runanywhere_vlm.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/extensions/runanywhere_storage.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dartsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVLM.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/VlmStreamEventMapper.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/extensions/VLM/RunAnywhereVisionLanguage.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/types/SwiftAliases.ktsdk/runanywhere-kotlin/src/test/kotlin/com/runanywhere/sdk/public/extensions/VLM/VLMGeneratedStreamSurfaceTest.ktsdk/runanywhere-react-native/AGENTS.mdsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+ProtoCompat.hppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Registry.cppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Tools.cppsdk/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); |
There was a problem hiding this comment.
🧩 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.dartRepository: 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 || trueRepository: 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 || trueRepository: 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
doneRepository: 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 || trueRepository: 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
_prefixKeybehavior 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.
| 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 {}, | ||
| ), | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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 {}, | ||
| ), | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| _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(); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
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.
| _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'); | ||
| } | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.



Description
Brief description of the changes made.
Type of Change
Testing
Platform-Specific Testing (check all that apply)
Swift SDK / iOS Sample:
Kotlin SDK / Android Sample:
Flutter SDK / Flutter Sample:
React Native SDK / React Native Sample:
Playground:
Web SDK / Web Sample:
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
Screenshots
Attach relevant UI screenshots for changes (if applicable):
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/ONNXregister beforeRunAnywhere.initialize(); failed setup surfacesInitErrorScreenwith retry. Model bootstrap re-registers the full catalog each launch (merge preserves downloads), callsrefreshModelRegistry(), 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
racApiErrorFromResponseandracModelIdFromUrl; HTTP client maps structured API errors and OkHttp timeouts match URLSession. Hybrid STT normalizes raw PCM16 to WAV for shared offline/online dispatch;pcm16ToWavis public. Init/reset wiresCppBridgeState(includingracStateReseton 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
Improvements