Skip to content

Add callback registration for TTS, VAD, and Voice Agent components#335

Open
siddhant-0707 wants to merge 3 commits intoRunanywhereAI:mainfrom
siddhant-0707:jni-bridge-callback
Open

Add callback registration for TTS, VAD, and Voice Agent components#335
siddhant-0707 wants to merge 3 commits intoRunanywhereAI:mainfrom
siddhant-0707:jni-bridge-callback

Conversation

@siddhant-0707
Copy link
Contributor

@siddhant-0707 siddhant-0707 commented Feb 5, 2026

Description

Implement JNI callback registration for VAD/TTS/VoiceAgent and wire Kotlin bridge registration/unregistration. Add VoiceAgent JNI declaration in RunAnywhereBridge.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Refactoring

Testing

  • Lint passes locally
  • Added/updated tests for changes

Platform-Specific Testing (check all that apply)

Kotlin SDK / Android Sample:

  • Tested on Android Phone (Emulator or Device)

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)
  • Commons - Changes to shared native code (sdk/runanywhere-commons)

Checklist

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

Important

Implement callback registration for TTS, VAD, and Voice Agent components in JNI and Kotlin bridge.

  • Callback Registration:
    • Implement callback registration for TTS, VAD, and Voice Agent in runanywhere_commons_jni.cpp.
    • Add racTtsSetCallbacks, racVadSetCallbacks, and racVoiceAgentSetCallbacks functions for managing callbacks.
  • Kotlin Bridge Integration:
    • Update CppBridgeTTS.kt, CppBridgeVAD.kt, and CppBridgeVoiceAgent.kt to call respective JNI functions for registration and unregistration.
    • Ensure callbacks are set to null during unregistration to prevent memory leaks.
  • Misc:
    • Add racVoiceAgentSetCallbacks declaration in RunAnywhereBridge.kt.

This description was created by Ellipsis for bb9beb1. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features
    • Added voice-agent callbacks and streaming TTS audio callbacks to surface partial transcriptions, tokens, audio chunks, and stream data to the app.
  • Improvements
    • Kotlin bridge components now register/unregister VAD, TTS, and Voice Agent callbacks through the native bridge for consistent lifecycle handling.
  • Bug Fixes
    • Improved cleanup and thread-safe callback handling to reduce leaks and registration failures.

Greptile Overview

Greptile Summary

  • Implements JNI callback registration for TTS, VAD, and VoiceAgent by storing global refs + method IDs in runanywhere_commons_jni.cpp.
  • Updates Kotlin bridge extensions (CppBridgeTTS/VAD/VoiceAgent) to register/unregister callbacks via RunAnywhereBridge.*SetCallbacks and clear them to avoid leaks.
  • Adds streaming adapters for TTS audio chunks and VAD activity events, and introduces a VoiceAgent stream-processing JNI entrypoint.
  • Noted integration gaps: VAD stream path sets native activity callback before handle validation, and the new VoiceAgent stream-processing JNI function isn’t exposed via RunAnywhereBridge.kt.

Confidence Score: 3/5

  • This PR is mergeable after addressing a couple of concrete JNI/Kotlin integration issues.
  • Most changes are straightforward callback wiring and cleanup, but there is at least one definite invalid-handle call path introduced in VAD streaming, and a new native voice-agent JNI entrypoint is not exposed in the Kotlin bridge which suggests an API mismatch/incomplete wiring.
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp; sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt

Important Files Changed

Filename Overview
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp Adds global callback registration state for TTS/VAD/VoiceAgent, adapters for streaming callbacks, and new JNI entrypoints; found a concrete crash/UB path where VAD sets activity callback before validating handle, and a new voice-agent JNI entrypoint appears without Kotlin bridge exposure.
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeTTS.kt Wires Kotlin TTS bridge registration/unregistration to native racTtsSetCallbacks by passing this callbacks and clearing them with nulls; no additional must-fix issues found in this diff.
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVAD.kt Updates Kotlin VAD bridge to register/unregister callbacks via native racVadSetCallbacks; no additional must-fix issues found in this diff.
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVoiceAgent.kt Imports RunAnywhereBridge and wires VoiceAgent callback registration/unregistration to racVoiceAgentSetCallbacks; no additional must-fix issues found in this diff.
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt Adds racVoiceAgentSetCallbacks external JNI declaration; found the new native racVoiceAgentProcessStream JNI entrypoint is not declared here, leaving that functionality inaccessible from Kotlin and risking API mismatch.

Sequence Diagram

sequenceDiagram
    participant App as Kotlin bridge (CppBridge*)
    participant JBridge as RunAnywhereBridge (JNI externals)
    participant JNI as runanywhere_commons_jni.cpp
    participant Core as RAC C/C++ core

    App->>JBridge: racTtsSetCallbacks(audioCb, progressCb)
    JBridge->>JNI: Java_*_racTtsSetCallbacks(...)
    JNI->>JNI: Store GlobalRefs + methodIDs

    App->>JBridge: racVadSetCallbacks(frameCb, startCb, endCb, progressCb)
    JBridge->>JNI: Java_*_racVadSetCallbacks(...)
    JNI->>JNI: Store GlobalRefs + methodIDs

    App->>JBridge: racVoiceAgentSetCallbacks(stateCb, phaseCb, ...)
    JBridge->>JNI: Java_*_racVoiceAgentSetCallbacks(...)
    JNI->>JNI: Store GlobalRefs + methodIDs

    App->>JBridge: racTtsComponentSynthesizeStream(handle, text, options)
    JBridge->>JNI: Java_*_racTtsComponentSynthesizeStream(...)
    JNI->>Core: rac_tts_component_synthesize_stream(..., tts_stream_adapter)
    Core-->>JNI: tts_stream_adapter(audioChunk)
    JNI-->>App: streamAudioCallback(byte[], isFinal=false)
    JNI-->>App: streamAudioCallback(byte[0], isFinal=true)

    App->>JBridge: racVadComponentProcessStream(handle, samples, options)
    JBridge->>JNI: Java_*_racVadComponentProcessStream(...)
    JNI->>Core: rac_vad_component_set_activity_callback(..., vad_activity_adapter)
    JNI->>Core: rac_vad_component_process(...)
    Core-->>JNI: vad_activity_adapter(activity)
    JNI-->>App: speechStartCallback/speechEndCallback + streamFrameCallback

    Note over App,JBridge: VoiceAgent processing JNI exists (racVoiceAgentProcessStream)
    Note over App,JBridge: but Kotlin external decl is missing in this PR
Loading

Copy link

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

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

Important

Looks good to me! 👍

Reviewed everything up to bb9beb1 in 12 seconds. Click for details.
  • Reviewed 502 lines of code in 5 files
  • Skipped 0 files when reviewing.
  • Skipped posting 0 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.

Workflow ID: wflow_voxkN7tStwdZBivg

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Adds per-component JNI callback state and adapters for TTS, VAD, and VoiceAgent; implements racTts/Vad/VoiceAgentSetCallbacks and streaming/event adapters in the JNI layer; wires Kotlin bridge extensions to call the new RunAnywhereBridge racSetCallbacks bindings.

Changes

Cohort / File(s) Summary
JNI Core Callback State & Adapters
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
Introduce VadCallbackState, TtsCallbackState, VoiceAgentCallbackState and TtsStreamContext; add static instances g_vad_callbacks, g_tts_callbacks, g_voice_agent_callbacks; implement adapter callbacks (tts_stream_adapter, vad_activity_adapter, voice_agent_event_adapter), helpers (getJNIEnv, clearPendingException, nowEpochMs), and cleanup in JNI_OnUnload.
JNI Callback Registration & Processing
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
Implement JNI entrypoints: racTtsSetCallbacks, racTtsComponentSynthesizeStream, racVadSetCallbacks, racVadComponentProcessStream, racVoiceAgentSetCallbacks, racVoiceAgentProcessStream; manage NewGlobalRef/DeleteGlobalRef, resolve jmethodIDs under mutex, and dispatch events to Java callbacks.
Kotlin Bridge Extensions
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeTTS.kt, .../CppBridgeVAD.kt, .../CppBridgeVoiceAgent.kt
Replace placeholder native registration/unregistration with calls to RunAnywhereBridge.racTtsSetCallbacks, racVadSetCallbacks, and racVoiceAgentSetCallbacks, passing component callback objects on register() and null on unregister().
Kotlin RunAnywhereBridge Interface
sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt
Add public JNI binding racVoiceAgentSetCallbacks(stateCallback, turnPhaseCallback, partialTranscriptionCallback, responseTokenCallback, audioChunkCallback, userInterruptCallback, progressCallback) to expose VoiceAgent callback registration from Kotlin.

Sequence Diagram

sequenceDiagram
    participant Kotlin as Kotlin Extension<br/>(CppBridge)
    participant Bridge as RunAnywhereBridge
    participant JNI as JNI Layer<br/>(runanywhere_commons_jni.cpp)
    participant JVM as Java VM

    Note over Kotlin,JVM: Callback Registration Flow

    Kotlin->>Bridge: racTtsSetCallbacks(audioCb, progressCb)
    Bridge->>JNI: native racTtsSetCallbacks call

    rect rgba(100,150,200,0.5)
        JNI->>JVM: GetJavaVM() → cache g_tts_callbacks.jvm
        JNI->>JVM: NewGlobalRef(audioCb)
        JNI->>JVM: NewGlobalRef(progressCb)
        JNI->>JVM: GetMethodID(streamAudioCallback)
        JNI->>JVM: GetMethodID(progressCallback)
    end

    JNI-->>Bridge: registration complete
    Bridge-->>Kotlin: return

    Note over Kotlin,JVM: Callback Unregistration Flow

    Kotlin->>Bridge: racTtsSetCallbacks(null, null)
    Bridge->>JNI: native racTtsSetCallbacks call

    rect rgba(200,100,150,0.5)
        JNI->>JVM: DeleteGlobalRef(audioCb)
        JNI->>JVM: DeleteGlobalRef(progressCb)
        JNI->>JNI: clear g_tts_callbacks state
    end

    JNI-->>Bridge: cleanup complete
    Bridge-->>Kotlin: return
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

kotlin-sdk, kotlin-sample, android-sample

Poem

🐰 In JNI burrows I hop and weave,
refs and callbacks snugly cleave,
Kotlin whispers, native sings,
audio, tokens, VAD all bring,
a rabbit cheers — the bridge's eve!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: implementing callback registration for three major components (TTS, VAD, Voice Agent). It is concise, specific, and clearly summarizes the main objective.
Description check ✅ Passed The PR description includes all required sections: Description, Type of Change (marked), Testing (partial), Platform-specific Testing (Android checked), Labels (Kotlin SDK and Commons checked), and Checklist items. The description clearly outlines the implementation of JNI callback registration and Kotlin bridge wiring.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 1506 to 1513
env->GetJavaVM(&g_tts_callbacks.jvm);

if (audioCallback != nullptr) {
g_tts_callbacks.audio_callback = env->NewGlobalRef(audioCallback);
jclass callbackClass = env->GetObjectClass(audioCallback);
g_tts_callbacks.audio_method =
env->GetMethodID(callbackClass, "streamAudioCallback", "([BZ)Z");
env->DeleteLocalRef(callbackClass);
Copy link
Contributor

Choose a reason for hiding this comment

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

Unhandled JNI pending exceptions

In rac*Tts/Vad/VoiceAgent*SetCallbacks, GetMethodID failures can leave a pending Java exception (e.g., signature mismatch). Right now the code only checks for a null jmethodID and keeps going, which can cause subsequent JNI calls to fail/abort with the original exception still pending. After each GetMethodID (and GetObjectClass), check/clear env->ExceptionCheck() (or return early after logging) so registration can’t proceed with a pending exception.

Also appears in: sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp:1712-1716, :1823-1827, and the other GetMethodID sites in those functions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
Line: 1506:1513

Comment:
**Unhandled JNI pending exceptions**

In `rac*Tts/Vad/VoiceAgent*SetCallbacks`, `GetMethodID` failures can leave a pending Java exception (e.g., signature mismatch). Right now the code only checks for a null `jmethodID` and keeps going, which can cause subsequent JNI calls to fail/abort with the original exception still pending. After each `GetMethodID` (and `GetObjectClass`), check/clear `env->ExceptionCheck()` (or return early after logging) so registration can’t proceed with a pending exception.

Also appears in: `sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp:1712-1716`, `:1823-1827`, and the other `GetMethodID` sites in those functions.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 5, 2026

Additional Comments (3)

sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
Leaked global JNI refs

JNI_OnUnload only deletes g_platform_adapter but never releases the new global callback refs (g_tts_callbacks.*, g_vad_callbacks.*, g_voice_agent_callbacks.*). If the library is unloaded/reloaded (tests, dynamic feature modules, etc.), those refs keep Java objects alive and leak memory. Clean up all non-null callback global refs and null their method IDs/state in JNI_OnUnload as well.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
Line: 147:159

Comment:
**Leaked global JNI refs**

`JNI_OnUnload` only deletes `g_platform_adapter` but never releases the new global callback refs (`g_tts_callbacks.*`, `g_vad_callbacks.*`, `g_voice_agent_callbacks.*`). If the library is unloaded/reloaded (tests, dynamic feature modules, etc.), those refs keep Java objects alive and leak memory. Clean up all non-null callback global refs and null their method IDs/state in `JNI_OnUnload` as well.

How can I resolve this? If you propose a fix, please make it concise.

sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVAD.kt
Wrong arg passed to JNI

loadModel(modelPath, modelId, ...) calls RunAnywhereBridge.racVadComponentLoadModel(handle, modelPath, config.toJson()), but the JNI signature is racVadComponentLoadModel(handle: Long, modelId: String?, configJson: String?) (RunAnywhereBridge.kt:299). This means you’re passing the path as the modelId, and the real modelId is ignored entirely. Fix the call site to pass modelId (and adjust JNI side if it actually needs a path).

This is not just naming: it changes what native sees and breaks telemetry/registry assumptions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVAD.kt
Line: 606:610

Comment:
**Wrong arg passed to JNI**

`loadModel(modelPath, modelId, ...)` calls `RunAnywhereBridge.racVadComponentLoadModel(handle, modelPath, config.toJson())`, but the JNI signature is `racVadComponentLoadModel(handle: Long, modelId: String?, configJson: String?)` (`RunAnywhereBridge.kt:299`). This means you’re passing the *path* as the *modelId*, and the real `modelId` is ignored entirely. Fix the call site to pass `modelId` (and adjust JNI side if it actually needs a path).

This is not just naming: it changes what native sees and breaks telemetry/registry assumptions.

How can I resolve this? If you propose a fix, please make it concise.

sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVoiceAgent.kt
Missing native library guard

create() calls nativeCreate() directly. Unlike TTS/VAD, there’s no check that the JNI library is actually loaded/available, so apps that don’t bundle the native libs will get an UnsatisfiedLinkError at runtime instead of a controlled SDKError.notInitialized(...). Add the same native-library availability check/try-catch pattern used in CppBridgeTTS.create/CppBridgeVAD.create before calling nativeCreate().

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVoiceAgent.kt
Line: 741:753

Comment:
**Missing native library guard**

`create()` calls `nativeCreate()` directly. Unlike TTS/VAD, there’s no check that the JNI library is actually loaded/available, so apps that don’t bundle the native libs will get an `UnsatisfiedLinkError` at runtime instead of a controlled `SDKError.notInitialized(...)`. Add the same native-library availability check/try-catch pattern used in `CppBridgeTTS.create`/`CppBridgeVAD.create` before calling `nativeCreate()`.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp`:
- Around line 95-135: The global callback state structs (VadCallbackState,
TtsCallbackState, VoiceAgentCallbackState) and their instances (g_vad_callbacks,
g_tts_callbacks, g_voice_agent_callbacks) are accessed concurrently by native
callback threads while registration/unregistration mutates and deletes Java
global refs; add synchronization to prevent races/use-after-free by embedding a
std::mutex (or std::shared_mutex) in each struct, acquire the mutex in all
setter/unregister functions that modify jobjects or jmethodIDs and also acquire
it (or take an atomic snapshot under the lock) at the start of every callback
invocation path before using
frame_callback/speech_start_callback/audio_callback/state_callback/etc. so that
DeleteGlobalRef and method calls cannot race. Ensure you hold the lock while
copying jobject/method IDs out (or incrementing ref counts) and release it
before making JNI calls if desired, or keep it held if needed to guarantee
safety, and update all functions that touch these symbols (e.g., where
registration/unregistration and callback dispatch reference g_vad_callbacks.*,
g_tts_callbacks.*, g_voice_agent_callbacks.*) to use the new mutex.
- Around line 1495-1560: The JNI currently populates g_tts_callbacks in
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racTtsSetCallbacks but
never wires them into the C API; create C-side adapter functions (e.g.,
tts_stream_adapter and tts_progress_adapter) that attach to the stored
g_tts_callbacks.jvm, convert parameters and call the stored Java methods via
CallBooleanMethod/CallVoidMethod using g_tts_callbacks.audio_method and
g_tts_callbacks.progress_method, then pass those adapters into the
rac_tts_synthesize_stream (or synthesize_stream) call instead of the blocking
synthesize(), and ensure global refs are deleted in JNI_OnUnload; apply the same
adapter + synthesize_stream + cleanup pattern to g_vad_callbacks and
g_voice_agent_callbacks (create VAD and VoiceAgent adapters, use the
streaming/event APIs) so callbacks are actually invoked and cleaned up on
unload.

- Introduced TTS stream adapter to handle audio data and invoke callbacks.
- Implemented VAD activity adapter to manage speech start and end events.
- Added mutexes to callback states for thread safety.
- Included forward declarations for helper functions to improve code organization.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp`:
- Around line 181-187: NewByteArray can return nullptr on OOM; add a null check
after jbyteArray jAudio = env->NewByteArray(...) to avoid calling
SetByteArrayRegion on a null pointer: if jAudio == nullptr, call
clearPendingException(env, "NewByteArray"), delete the callback local ref
(env->DeleteLocalRef(callback)), and return early from tts_stream_adapter;
otherwise continue with
SetByteArrayRegion/CallBooleanMethod/DeleteLocalRef(jAudio). Apply the same
pattern in voice_agent_event_adapter for its NewByteArray usage.
- Around line 1710-1725: The code is invoking Java (env->CallBooleanMethod)
while holding g_tts_callbacks.mutex which can deadlock if the Java callback
re-enters racTtsSetCallbacks; fix by copying the needed references under the
lock and performing the JNI call and local-ref deletions after releasing the
lock: inside the scope guarded by g_tts_callbacks.mutex, read
g_tts_callbacks.audio_callback and g_tts_callbacks.audio_method into local
variables (creating a NewLocalRef for audio_callback if non-null) and then drop
the lock; after the lock scope, if the local callback and method are set, create
the empty jbyteArray, call env->CallBooleanMethod, call
clearPendingException(env, "racTtsComponentSynthesizeStream final callback"),
and finally delete the local refs (env->DeleteLocalRef for the jbyteArray and
callback).
🧹 Nitpick comments (4)
sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp (4)

1849-1860: Inconsistent indentation in the else blocks.

Throughout racTtsSetCallbacks, racVadSetCallbacks, and racVoiceAgentSetCallbacks, the code inside the else blocks is under-indented relative to the braces (e.g., lines 1850–1859 vs. the enclosing else { at line 1849). The same pattern repeats in every callback registration block. This makes the logic flow harder to follow during review.


442-455: getJNIEnv() attaches native threads without ever detaching them.

AttachCurrentThread is called at line 450, but there's no corresponding DetachCurrentThread. Every native callback thread that invokes tts_stream_adapter, vad_activity_adapter, or voice_agent_event_adapter through this helper gets permanently attached. If the C API spawns short-lived threads for callbacks, these will leak JVM resources when the native thread exits without detaching.

Consider either:

  • Tracking whether the thread was attached by this call and detaching after the callback completes (as done in llm_stream_callback_token at line 1042–1067), or
  • Using a thread-local destructor (via pthread_key_create) to auto-detach on thread exit.

1816-1883: ~400 lines of copy-paste callback registration boilerplate.

Each racTtsSetCallbacks, racVadSetCallbacks, and racVoiceAgentSetCallbacks repeats the identical per-callback pattern: delete old global ref → NewGlobalRefGetObjectClassGetMethodID → error cleanup. A small helper would collapse this dramatically:

static bool registerJniCallback(JNIEnv* env, jobject javaCallback,
                                const char* methodName, const char* methodSig,
                                jobject& outGlobalRef, jmethodID& outMethod) {
    outGlobalRef = env->NewGlobalRef(javaCallback);
    jclass cls = env->GetObjectClass(javaCallback);
    if (clearPendingException(env, "registerJniCallback GetObjectClass") || !cls) {
        env->DeleteGlobalRef(outGlobalRef);
        outGlobalRef = nullptr;
        return false;
    }
    outMethod = env->GetMethodID(cls, methodName, methodSig);
    env->DeleteLocalRef(cls);
    if (clearPendingException(env, "registerJniCallback GetMethodID") || !outMethod) {
        env->DeleteGlobalRef(outGlobalRef);
        outGlobalRef = nullptr;
        outMethod = nullptr;
        return false;
    }
    return true;
}

Also applies to: 2032-2156, 2158-2370


103-147: jvm field in each callback state struct is set but never read.

g_tts_callbacks.jvm, g_vad_callbacks.jvm, and g_voice_agent_callbacks.jvm are populated during registration (lines 1839, 2067, 2212) but all three adapter functions use the global getJNIEnv() (which reads g_jvm) instead. These fields are dead state. Consider removing them to avoid confusion, or switching the adapters to use them for consistency.

Comment on lines +181 to +187
jbyteArray jAudio = env->NewByteArray(static_cast<jsize>(audio_size));
env->SetByteArrayRegion(jAudio, 0, static_cast<jsize>(audio_size),
reinterpret_cast<const jbyte*>(audio_data));
env->CallBooleanMethod(callback, method, jAudio, JNI_FALSE);
env->DeleteLocalRef(jAudio);
env->DeleteLocalRef(callback);
clearPendingException(env, "tts_stream_adapter CallBooleanMethod");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing null check on NewByteArray return — potential JVM crash on OOM.

env->NewByteArray() can return nullptr on allocation failure (e.g., large audio chunks). Calling SetByteArrayRegion on a null array will crash the JVM. The same pattern also appears in voice_agent_event_adapter (line 328).

Proposed fix
     jbyteArray jAudio = env->NewByteArray(static_cast<jsize>(audio_size));
+    if (!jAudio) {
+        env->DeleteLocalRef(callback);
+        clearPendingException(env, "tts_stream_adapter NewByteArray OOM");
+        return;
+    }
     env->SetByteArrayRegion(jAudio, 0, static_cast<jsize>(audio_size),
                             reinterpret_cast<const jbyte*>(audio_data));
🤖 Prompt for AI Agents
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines 181
- 187, NewByteArray can return nullptr on OOM; add a null check after jbyteArray
jAudio = env->NewByteArray(...) to avoid calling SetByteArrayRegion on a null
pointer: if jAudio == nullptr, call clearPendingException(env, "NewByteArray"),
delete the callback local ref (env->DeleteLocalRef(callback)), and return early
from tts_stream_adapter; otherwise continue with
SetByteArrayRegion/CallBooleanMethod/DeleteLocalRef(jAudio). Apply the same
pattern in voice_agent_event_adapter for its NewByteArray usage.

Comment on lines +269 to +347
static void voice_agent_event_adapter(const rac_voice_agent_event_t* event, void* user_data) {
(void)user_data;
if (!event) {
return;
}

JNIEnv* env = getJNIEnv();
if (!env) {
return;
}

jobject partial_cb = nullptr;
jobject response_cb = nullptr;
jobject audio_cb = nullptr;
jmethodID partial_method = nullptr;
jmethodID response_method = nullptr;
jmethodID audio_method = nullptr;

{
std::lock_guard<std::mutex> lock(g_voice_agent_callbacks.mutex);
if (g_voice_agent_callbacks.partial_transcription_callback &&
g_voice_agent_callbacks.partial_transcription_method) {
partial_cb = env->NewLocalRef(g_voice_agent_callbacks.partial_transcription_callback);
partial_method = g_voice_agent_callbacks.partial_transcription_method;
}
if (g_voice_agent_callbacks.response_token_callback &&
g_voice_agent_callbacks.response_token_method) {
response_cb = env->NewLocalRef(g_voice_agent_callbacks.response_token_callback);
response_method = g_voice_agent_callbacks.response_token_method;
}
if (g_voice_agent_callbacks.audio_chunk_callback &&
g_voice_agent_callbacks.audio_chunk_method) {
audio_cb = env->NewLocalRef(g_voice_agent_callbacks.audio_chunk_callback);
audio_method = g_voice_agent_callbacks.audio_chunk_method;
}
}

switch (event->type) {
case RAC_VOICE_AGENT_EVENT_TRANSCRIPTION: {
if (partial_cb && partial_method && event->data.transcription) {
jstring jText = env->NewStringUTF(event->data.transcription);
env->CallVoidMethod(partial_cb, partial_method, jText);
env->DeleteLocalRef(jText);
clearPendingException(env, "voice_agent_event_adapter partialTranscriptionCallback");
}
break;
}
case RAC_VOICE_AGENT_EVENT_RESPONSE: {
if (response_cb && response_method && event->data.response) {
jstring jToken = env->NewStringUTF(event->data.response);
env->CallBooleanMethod(response_cb, response_method, jToken, JNI_TRUE);
env->DeleteLocalRef(jToken);
clearPendingException(env, "voice_agent_event_adapter responseTokenCallback");
}
break;
}
case RAC_VOICE_AGENT_EVENT_AUDIO_SYNTHESIZED: {
if (audio_cb && audio_method && event->data.audio.audio_data &&
event->data.audio.audio_size > 0) {
jbyteArray jAudio = env->NewByteArray(static_cast<jsize>(event->data.audio.audio_size));
env->SetByteArrayRegion(jAudio, 0, static_cast<jsize>(event->data.audio.audio_size),
reinterpret_cast<const jbyte*>(event->data.audio.audio_data));
env->CallBooleanMethod(audio_cb, audio_method, jAudio, JNI_TRUE);
env->DeleteLocalRef(jAudio);
clearPendingException(env, "voice_agent_event_adapter audioChunkCallback");
}
break;
}
default:
break;
}

if (partial_cb)
env->DeleteLocalRef(partial_cb);
if (response_cb)
env->DeleteLocalRef(response_cb);
if (audio_cb)
env->DeleteLocalRef(audio_cb);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

voice_agent_event_adapter only routes 3 of 7 registered callback types.

The adapter handles TRANSCRIPTION, RESPONSE, and AUDIO_SYNTHESIZED events, but stateCallback, turnPhaseCallback, userInterruptCallback, and progressCallback are registered and stored (global refs created, method IDs cached) yet never invoked. Their global refs and local-ref snapshots are acquired but the corresponding event types are silently dropped by the default: break;. Either add cases for the remaining event types or avoid registering/caching those callbacks to prevent leaking attention and global refs for dead code.

Also, JNI_TRUE is hardcoded as the isFinal/isLast flag on lines 319 and 331 for every event. If the C API delivers incremental tokens/chunks, the Kotlin side will treat each one as the final piece.

Comment on lines +1710 to +1725
{
jobject callback = nullptr;
jmethodID method = nullptr;
std::lock_guard<std::mutex> lock(g_tts_callbacks.mutex);
if (g_tts_callbacks.audio_callback && g_tts_callbacks.audio_method) {
callback = env->NewLocalRef(g_tts_callbacks.audio_callback);
method = g_tts_callbacks.audio_method;
}
if (callback && method) {
jbyteArray empty = env->NewByteArray(0);
env->CallBooleanMethod(callback, method, empty, JNI_TRUE);
env->DeleteLocalRef(empty);
env->DeleteLocalRef(callback);
clearPendingException(env, "racTtsComponentSynthesizeStream final callback");
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Deadlock risk: JNI call made while holding g_tts_callbacks.mutex.

The lock_guard at line 1713 holds the mutex until line 1725. CallBooleanMethod at line 1720 invokes Java while the lock is held. If the Java callback synchronously calls racTtsSetCallbacks (which also acquires this mutex), the thread will deadlock on the non-recursive std::mutex.

The streaming adapters (tts_stream_adapter, vad_activity_adapter) correctly copy refs under the lock and then call Java outside the lock. Apply the same pattern here.

Proposed fix
     {
         jobject callback = nullptr;
         jmethodID method = nullptr;
-        std::lock_guard<std::mutex> lock(g_tts_callbacks.mutex);
-        if (g_tts_callbacks.audio_callback && g_tts_callbacks.audio_method) {
-            callback = env->NewLocalRef(g_tts_callbacks.audio_callback);
-            method = g_tts_callbacks.audio_method;
-        }
-        if (callback && method) {
-            jbyteArray empty = env->NewByteArray(0);
-            env->CallBooleanMethod(callback, method, empty, JNI_TRUE);
-            env->DeleteLocalRef(empty);
-            env->DeleteLocalRef(callback);
-            clearPendingException(env, "racTtsComponentSynthesizeStream final callback");
+        {
+            std::lock_guard<std::mutex> lock(g_tts_callbacks.mutex);
+            if (g_tts_callbacks.audio_callback && g_tts_callbacks.audio_method) {
+                callback = env->NewLocalRef(g_tts_callbacks.audio_callback);
+                method = g_tts_callbacks.audio_method;
+            }
+        }
+        if (callback && method) {
+            jbyteArray empty = env->NewByteArray(0);
+            env->CallBooleanMethod(callback, method, empty, JNI_TRUE);
+            env->DeleteLocalRef(empty);
+            env->DeleteLocalRef(callback);
+            clearPendingException(env, "racTtsComponentSynthesizeStream final callback");
         }
     }
🤖 Prompt for AI Agents
In `@sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp` around lines
1710 - 1725, The code is invoking Java (env->CallBooleanMethod) while holding
g_tts_callbacks.mutex which can deadlock if the Java callback re-enters
racTtsSetCallbacks; fix by copying the needed references under the lock and
performing the JNI call and local-ref deletions after releasing the lock: inside
the scope guarded by g_tts_callbacks.mutex, read g_tts_callbacks.audio_callback
and g_tts_callbacks.audio_method into local variables (creating a NewLocalRef
for audio_callback if non-null) and then drop the lock; after the lock scope, if
the local callback and method are set, create the empty jbyteArray, call
env->CallBooleanMethod, call clearPendingException(env,
"racTtsComponentSynthesizeStream final callback"), and finally delete the local
refs (env->DeleteLocalRef for the jbyteArray and callback).

@shubhammalhotra28
Copy link
Contributor

@coderabbitai please do a detailed review. Thanks!

@shubhammalhotra28
Copy link
Contributor

@greptile please do a detailed review. Thanks!

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the main JNI implementation file to understand the callback registration implementation
cat sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp

Length of output: 170887


🏁 Script executed:

#!/bin/bash
# Read the Kotlin bridge files to understand how callbacks are registered
cat sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeTTS.kt

Length of output: 47873


🏁 Script executed:

#!/bin/bash
cat sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVAD.kt

Length of output: 45214


🏁 Script executed:

#!/bin/bash
cat sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeVoiceAgent.kt

Length of output: 57348


🏁 Script executed:

#!/bin/bash
cat sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt

Length of output: 29535


@shubhammalhotra28 Thanks for the request — here’s a detailed review of PR #335. I also incorporated a prior learning we have on this repo about the voice agent needing thread-safe pipeline orchestration (we validated the mutex-based design against that).

What’s working well

  • Thread-safety: Per-component mutexes with copy-out of callback refs (via NewLocalRef) before invoking Java is the right pattern.
  • Lifecycle: Global refs are released for TTS/VAD/VoiceAgent on JNI_OnUnload; callback registration cleans previous globals first.
  • Exception hygiene during callback invocations: clearPendingException() used after Call*Method to prevent sticky exceptions.
  • TTS streaming: on-chunk callback + final empty chunk with isFinal=true matches the Kotlin StreamCallback contract.

Must-fix before merge

  1. VAD audio sample type mismatch (corrupt input)
  • Issue: JNI reinterprets a Java byte[] as float* unconditionally:
    rac_vad_component_process(..., reinterpret_cast<const float*>(data), len/sizeof(float), ...)
    Kotlin defaults to PCM_16, so bytes represent Int16, not Float32.
  • Impact: Incorrect probabilities/events; potential crashes on odd len alignment.
  • Fix (minimal, JSON-driven): Inspect optionsJson for "audio_format" (0=PCM_16, 1=PCM_FLOAT), convert when needed.

C++ patch sketch:

// helper
static int parse_audio_format(const char* json) {
    if (!json) return 0; // default PCM_16
    const char* key = "\"audio_format\":";
    const char* p = strstr(json, key);
    if (!p) return 0;
    p += strlen(key);
    while (*p == ' ') ++p;
    return atoi(p); // 0 or 1
}

JNIEXPORT jstring JNICALL
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racVadComponentProcess(
    JNIEnv* env, jclass, jlong handle, jbyteArray audioData, jstring configJson) {
    if (handle == 0 || audioData == nullptr) return nullptr;

    jsize len = env->GetArrayLength(audioData);
    jbyte* data = env->GetByteArrayElements(audioData, nullptr);

    // Read audio_format from configJson
    const char* json = nullptr;
    if (configJson) json = env->GetStringUTFChars(configJson, nullptr);
    int audio_format = parse_audio_format(json); // 0=PCM_16, 1=PCM_FLOAT

    rac_bool_t out_is_speech = RAC_FALSE;
    rac_result_t status = RAC_ERROR_INVALID_ARGUMENT;

    if (audio_format == 1) {
        // Float32 path: len must be multiple of 4
        size_t count = static_cast<size_t>(len) / sizeof(float);
        status = rac_vad_component_process(
            reinterpret_cast<rac_handle_t>(handle),
            reinterpret_cast<const float*>(data),
            count, &out_is_speech);
    } else {
        // Int16 -> Float32 conversion
        size_t count = static_cast<size_t>(len) / sizeof(int16_t);
        std::vector<float> f(count);
        const int16_t* s = reinterpret_cast<const int16_t*>(data);
        for (size_t i = 0; i < count; ++i) f[i] = static_cast<float>(s[i]) / 32768.0f;
        status = rac_vad_component_process(
            reinterpret_cast<rac_handle_t>(handle),
            f.data(), f.size(), &out_is_speech);
    }

    if (configJson && json) env->ReleaseStringUTFChars(configJson, json);
    env->ReleaseByteArrayElements(audioData, data, JNI_ABORT);

    if (status != RAC_SUCCESS) return nullptr;

    char jsonBuf[256];
    snprintf(jsonBuf, sizeof(jsonBuf), "{\"is_speech\":%s,\"probability\":%.4f}",
             out_is_speech ? "true" : "false", out_is_speech ? 1.0f : 0.0f);
    return env->NewStringUTF(jsonBuf);
}
  • Apply the same handling to racVadComponentProcessStream and racVadComponentProcessFrame (they delegate today, which is good).
  1. Safe thread attachment/detachment
  • Issue: getJNIEnv() attaches on JNI_EDETACHED but never detaches. For native threads that terminate, this can leak JVM thread resources.
  • Fix: Use an RAII scope that detaches only if we attached.

C++ patch sketch:

struct JniEnvScope {
    JNIEnv* env = nullptr;
    bool attached = false;
    JniEnvScope() {
        if (!g_jvm) return;
        jint st = g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
        if (st == JNI_EDETACHED) {
            if (g_jvm->AttachCurrentThread(&env, nullptr) == JNI_OK) attached = true;
            else env = nullptr;
        }
    }
    ~JniEnvScope() { if (attached && g_jvm) g_jvm->DetachCurrentThread(); }
    JNIEnv* get() const { return env; }
};
// usage in adapters:
static void tts_stream_adapter(const void* audio_data, size_t audio_size, void* user_data) {
    JniEnvScope es; JNIEnv* env = es.get(); if (!env) return;
    ...
}
  1. VoiceAgent.create(): add native-lib availability guard
  • Issue: Unlike TTS/VAD, VoiceAgent.create() calls nativeCreate() without checking native library availability. This can throw UnsatisfiedLinkError.
  • Fix (Kotlin):
fun create(): Int {
    synchronized(lock) {
        if (handle != 0L) return 0

        // Match TTS/VAD pattern
        if (!CppBridge.isNativeLibraryLoaded) {
            CppBridgePlatformAdapter.logCallback(
                CppBridgePlatformAdapter.LogLevel.ERROR, TAG,
                "Native library not loaded. VoiceAgent requires native libs.")
            throw SDKError.notInitialized("Native library not available. Bundle the native libs.")
        }

        val result = try { nativeCreate() } catch (e: UnsatisfiedLinkError) {
            CppBridgePlatformAdapter.logCallback(
                CppBridgePlatformAdapter.LogLevel.ERROR, TAG,
                "Voice Agent creation failed: ${e.message}")
            throw SDKError.notInitialized("Voice Agent native lib not available.")
        }
        if (result == 0L) return -1
        handle = result
        setState(VoiceAgentState.CREATED)
        return 0
    }
}
  1. Platform adapter method caching: add exception/null checks
  • Issue: racSetPlatformAdapter() caches many method IDs but does not validate after GetMethodID calls. A missing method will cause a crash later.
  • Fix: After each GetMethodID, verify non-null and !ExceptionCheck(); if any fail, clear exception, delete global adapter ref, and return RAC_ERROR_INVALID_ARGUMENT.

C++ patch sketch:

`#define` GET_MID_OR_FAIL(var, cls, name, sig) \
    var = env->GetMethodID(cls, name, sig); \
    if (!var || env->ExceptionCheck()) { \
        env->ExceptionDescribe(); env->ExceptionClear(); \
        env->DeleteLocalRef(cls); \
        env->DeleteGlobalRef(g_platform_adapter); g_platform_adapter = nullptr; \
        return RAC_ERROR_INVALID_ARGUMENT; \
    }

jclass adapterClass = env->GetObjectClass(adapter);
GET_MID_OR_FAIL(g_method_log, adapterClass, "log", "(ILjava/lang/String;Ljava/lang/String;)V")
GET_MID_OR_FAIL(g_method_file_exists, adapterClass, "fileExists", "(Ljava/lang/String;)Z")
// ... repeat for all cached methods
env->DeleteLocalRef(adapterClass);
  1. Return a local ref from racGetPlatformAdapter()
  • Issue: Returning the stored global directly is unconventional; safer to return a local ref (or null).
  • Fix:
JNIEXPORT jobject JNICALL
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racGetPlatformAdapter(JNIEnv* env, jclass) {
    std::lock_guard<std::mutex> lock(g_adapter_mutex);
    return g_platform_adapter ? env->NewLocalRef(g_platform_adapter) : nullptr;
}
  1. Voice agent token streaming: isFinal flag
  • Issue: voice_agent_event_adapter always calls responseTokenCallback(..., JNI_TRUE). This defeats token streaming control.
  • Fix: If the core event carries an “is_final” bit (often present in streaming APIs), pass it through; otherwise default to JNI_FALSE for intermediate tokens and issue a dedicated “final” event.

C++ patch sketch (assuming a flag exists):

case RAC_VOICE_AGENT_EVENT_RESPONSE: {
    if (response_cb && response_method && event->data.response) {
        jstring jToken = env->NewStringUTF(event->data.response);
        jboolean jIsFinal = event->is_final ? JNI_TRUE : JNI_FALSE; // or event->data.response_is_final
        env->CallBooleanMethod(response_cb, response_method, jToken, jIsFinal);
        env->DeleteLocalRef(jToken);
        clearPendingException(env, "... responseTokenCallback");
    }
    break;
}
  1. VAD activity mapping: avoid reporting speech during silence
  • Issue: In vad_activity_adapter, any non-start/non-end activity currently maps to SPEECH_ONGOING with is_speech=true. If the native enum includes a “silence/none” state, this over-reports.
  • Fix: Map RAC_SPEECH_NONE → SILENCE (is_speech=false), RAC_SPEECH_ONGOING → SPEECH_ONGOING (is_speech=true). Example:
switch (activity) {
    case RAC_SPEECH_STARTED: /* existing */ break;
    case RAC_SPEECH_ENDED:   /* existing */ break;
    case RAC_SPEECH_ONGOING:
        event_type = 2; is_speech = true; probability = 1.0f; break;
    case RAC_SPEECH_NONE:
    default:
        event_type = 0; is_speech = false; probability = 0.0f; break;
}
  1. Guard OOM paths in adapters
  • Issue: NewByteArray/NewStringUTF can throw OOM. In several adapters we directly Call*Method afterward.
  • Fix: Check for null or ExceptionCheck() immediately after allocation and early-return if allocation failed; also avoid calling into Java when env->ExceptionCheck() is true.

Nice-to-have improvements

  • TTS/VAD progress callbacks are registered but never invoked; wire into core progress signals if available, or remove until implemented to avoid confusion.
  • Kotlin naming: In RunAnywhereBridge.racVadComponentLoadModel, rename the second parameter to modelPath for clarity and update references.
  • Consider documenting that racLlmComponentGenerateStream and the VoiceAgent event adapter assume synchronous streaming (global ref deletion post-call). If async is introduced later, move global-ref lifetime to a scope that outlives the async work.

Tests and verification

  • VAD format tests:
    • PCM_16 and PCM_FLOAT inputs produce identical results on a known clip when configured appropriately.
    • Frame/stream paths both honor audio_format in optionsJson.
  • JNI lifecycle:
    • Stress test attach/detach with adapter callbacks from a native worker thread (start/stop threads repeatedly).
  • Voice agent streaming:
    • Verify that responseTokenCallback is invoked with isFinal=false for intermediate tokens and true once at the end.
  • Regression:
    • Confirm global refs are released on library unload (adb unload/reload or test process restart).

Overall take
This PR is a substantial step forward — the callback registration design and thread-safety approach are solid. Fixing the VAD sample-type mismatch and the JNI attach/detach hygiene are the two critical blockers. The remaining items will harden the bridge and improve correctness in edge cases.

Would you like me to prepare a small follow-up plan to land the must-fix patches above as a targeted commit? I can break it into:

  • JNI: VAD format handling + JniEnvScope + platform adapter checks + local-ref return
  • Kotlin: VoiceAgent.create() native-lib guard + rename param

🧠 Learnings used
Learnt from: CR
Repo: RunanywhereAI/runanywhere-sdks PR: 0
File: sdk/runanywhere-commons/CLAUDE.md:0-0
Timestamp: 2026-01-16T07:01:03.349Z
Learning: Applies to sdk/runanywhere-commons/src/features/voice_agent/*.{cpp,h} : Voice agent implementation must support configuration of individual components (LLM, STT, TTS, VAD) and thread-safe pipeline orchestration

Learnt from: CR
Repo: RunanywhereAI/runanywhere-sdks PR: 0
File: sdk/runanywhere-commons/CLAUDE.md:0-0
Timestamp: 2026-01-16T07:01:03.349Z
Learning: Applies to sdk/runanywhere-commons/src/backends/platform/*.{cpp,h} : Platform backend for Apple must use Swift callbacks for Apple Foundation Models (LLM) and System TTS
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 1959 to +1966
Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racVadComponentProcessStream(
JNIEnv* env, jclass clazz, jlong handle, jbyteArray audioData, jstring configJson) {
{
std::lock_guard<std::mutex> lock(g_vad_callbacks.mutex);
if (g_vad_callbacks.frame_callback || g_vad_callbacks.speech_start_callback ||
g_vad_callbacks.speech_end_callback) {
rac_vad_component_set_activity_callback(reinterpret_cast<rac_handle_t>(handle),
vad_activity_adapter, nullptr);
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing handle validation
racVadComponentProcessStream unconditionally calls rac_vad_component_set_activity_callback(reinterpret_cast<rac_handle_t>(handle), ...) before validating handle. If handle == 0 (or otherwise invalid), this introduces a new crash/UB path that wasn’t present before (the underlying racVadComponentProcess checks handle, but this call happens earlier). Add the same handle guard here before setting the callback.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
Line: 1959:1966

Comment:
**Missing handle validation**
`racVadComponentProcessStream` unconditionally calls `rac_vad_component_set_activity_callback(reinterpret_cast<rac_handle_t>(handle), ...)` before validating `handle`. If `handle == 0` (or otherwise invalid), this introduces a new crash/UB path that wasn’t present before (the underlying `racVadComponentProcess` checks `handle`, but this call happens earlier). Add the same handle guard here before setting the callback.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +340 to +352
// VOICE AGENT CALLBACKS (rac_voice_agent.h)
// ========================================================================

@JvmStatic
external fun racVoiceAgentSetCallbacks(
stateCallback: Any?,
turnPhaseCallback: Any?,
partialTranscriptionCallback: Any?,
responseTokenCallback: Any?,
audioChunkCallback: Any?,
userInterruptCallback: Any?,
progressCallback: Any?,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

JNI symbol not declared
The native side adds Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racVoiceAgentProcessStream(...) in runanywhere_commons_jni.cpp, but there is no corresponding external fun racVoiceAgentProcessStream(...) declaration in RunAnywhereBridge.kt. This makes the new JNI entrypoint unreachable from Kotlin and can also indicate API mismatch between layers. Add the missing external declaration (signature should match the JNI function params/return).

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/runanywhere-kotlin/src/jvmAndroidMain/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt
Line: 340:352

Comment:
**JNI symbol not declared**
The native side adds `Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racVoiceAgentProcessStream(...)` in `runanywhere_commons_jni.cpp`, but there is no corresponding `external fun racVoiceAgentProcessStream(...)` declaration in `RunAnywhereBridge.kt`. This makes the new JNI entrypoint unreachable from Kotlin and can also indicate API mismatch between layers. Add the missing external declaration (signature should match the JNI function params/return).

How can I resolve this? If you propose a fix, please make it concise.

@shubhammalhotra28
Copy link
Contributor

Hey @siddhant-0707
Can you rebase ? and address the merge conflict too ?

@Siddhesh2377
Copy link
Collaborator

Hey @siddhant-0707 Can you rebase ? and address the merge conflict too ?

There is no reply should this PR be closed ?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments