feat(connect): add macOS host support for mobile clients#556
feat(connect): add macOS host support for mobile clients#556yavuzkaanakyuz wants to merge 7 commits into
Conversation
📝 WalkthroughWalkthroughThis PR adds RunAnywhere Connect: a protobuf-defined LAN protocol with native ABI support, platform-specific discovery and framed TCP sessions, hosted-model streaming, and connection UI integrations across Android, Flutter, iOS, and React Native examples. ChangesConnect protocol and native core
Platform SDKs
Example applications
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Sequence Diagram(s)sequenceDiagram
participant User
participant ModelPicker
participant ConnectSession
participant LocalNetworkHost
participant ChatViewModel
User->>ModelPicker: Start Connect discovery
ModelPicker->>ConnectSession: startBrowsing()
ConnectSession->>LocalNetworkHost: Discover and resolve service
User->>ModelPicker: Select host
ModelPicker->>ConnectSession: connect(host)
ConnectSession->>LocalNetworkHost: Send hello and receive handshake
LocalNetworkHost-->>ConnectSession: Return session and model
ConnectSession-->>ChatViewModel: Publish connected state
ChatViewModel->>ConnectSession: generateStream(request)
ConnectSession->>LocalNetworkHost: Send invocation frame
LocalNetworkHost-->>ConnectSession: Stream invocation events
ConnectSession-->>ChatViewModel: Publish generated tokens
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai please do a detailed review |
00789d3 to
032d8b8
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (5)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_connect.dart (1)
48-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCentralize the Connect FFI lookups.
DartBridgeConnectresolves eachrac_connect_*symbol on every request. Move these bindings intonative_functions.dartso the function pointers are cached once and reused like the rest of the native bridge.🤖 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_connect.dart` around lines 48 - 61, Update DartBridgeConnect._call to stop resolving symbols through _library.lookupFunction on each request. Add and cache the Connect FFI function pointers in native_functions.dart, following the existing native bridge binding pattern, then have _call reuse the cached binding associated with each symbol while preserving request invocation and decoding behavior.Source: Path instructions
examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Models/ModelSelectionSheet.swift (1)
374-398: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winInline magic numbers in
connectRowLabelshould use centralized layout tokens.
size: 18,width: 38, height: 38,cornerRadius: 10, andspacing: 2are inline magic numbers in a view. Route these throughAppSpacing/AppLayout/AppTypography/AdaptiveSizing(e.g., anAppTypographyicon weight,AppLayouticon frame + corner radius,AppSpacingfor the label stack spacing) so Connect rows stay consistent with the rest of the sheet.As per coding guidelines: "Centralize colors, spacing, typography, layout constants, and adaptive interactive-element sizing in
AppColors,AppSpacing,AppTypography,AppLayout, andAdaptiveSizing; do not use inline magic numbers or color literals in views."🤖 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/Models/ModelSelectionSheet.swift` around lines 374 - 398, Update connectRowLabel to replace the inline icon size, frame dimensions, corner radius, and VStack spacing with the appropriate existing AppTypography, AppLayout, and AppSpacing tokens. Reuse centralized constants already defined for comparable model-selection rows, preserving the current appearance and layout.Source: Coding guidelines
examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart (1)
492-549: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate streaming-aggregation logic with
_generateStreaming.
_generateHostedStreamingre-implements almost all of_generateStreaming's token-aggregation, analytics-building, and persistence logic, differing only in the event source (ConnectService.shared.session.generateStreamvssdk.RunAnywhere.llm.generateStream). Consider extracting a shared private helper parameterized by theStream<LLMStreamEvent>source to avoid the duplication.🤖 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 492 - 549, Extract the shared token aggregation, analytics construction, persistence, and error-state handling from _generateStreaming and _generateHostedStreaming into a private helper parameterized by the Stream<LLMStreamEvent> source. Update both methods to supply their existing stream sources and preserve their current prompt/options behavior, message updates, and hosted-generation error handling.examples/flutter/RunAnywhereAI/lib/app/content_view.dart (1)
30-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBoth sites consume the shared
ConnectService.sharedsingleton via manualaddListener/setStateinstead ofListenableBuilder. The documented state-management pattern for sharedChangeNotifiersingletons isListenableBuilder; both widgets instead wire up manual listener registration/removal plus baresetState(() {}).
examples/flutter/RunAnywhereAI/lib/app/content_view.dart#L30-L46: wrap the_ConnectBanner/overlay consumption of_connect.statein aListenableBuilder(listenable: _connect, ...)and drop the manualaddListener/removeListener/_connectChanged.examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart#L81-L99: wrap_buildConnectSection's consumption of_connect.statein aListenableBuilder(listenable: _connect, ...)instead of the manual listener +setState, keeping the post-frameNavigator.popside effect in a listener callback if needed for the connected-dismiss behavior.As per coding guidelines, "Use the established state-management patterns: shared ChangeNotifier singletons with ListenableBuilder for feature-level state and 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/app/content_view.dart` around lines 30 - 46, Replace manual ConnectService.shared listener registration and bare setState usage in examples/flutter/RunAnywhereAI/lib/app/content_view.dart#L30-L46 with a ListenableBuilder around _ConnectBanner/overlay consumption of _connect.state, removing _connectChanged and its lifecycle hooks. In examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart#L81-L99, likewise wrap _buildConnectSection’s _connect.state consumption in ListenableBuilder and remove manual listener wiring; preserve the post-frame Navigator.pop behavior through a listener callback if required.Source: Coding guidelines
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx (1)
987-1007: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRaw color literals instead of theme tokens in
connectBanner.
backgroundColor: 'rgba(255,255,255,0.96)'andshadowColor: '#000000'bypassColors.*, used consistently elsewhere in this same style block (e.g.borderColor: Colors.borderLight) and file. These literals won't adapt if the theme supports dark mode.As per coding guidelines: "Avoid inline React Native styles; use the shared theme tokens from
colors.ts,typography.ts, andspacing.tswhere applicable."♻️ Proposed fix
- backgroundColor: 'rgba(255,255,255,0.96)', + backgroundColor: Colors.backgroundElevated, // or nearest existing token borderWidth: 1, borderColor: Colors.borderLight, - shadowColor: '`#000000`', + shadowColor: Colors.shadow ?? '`#000000`',🤖 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 987 - 1007, Update the connectBanner style to replace the raw backgroundColor and shadowColor literals with the appropriate shared Colors theme tokens, matching the existing theme usage in ChatScreen.tsx and preserving the banner’s visual intent across supported themes.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`:
- Around line 184-194: Update _ConnectBanner.didUpdateWidget to track the key
currently associated with a scheduled auto-dismiss timer, and only schedule the
6-second Future.delayed once per connected, non-dismissed key. Preserve the
existing key-change reset behavior and clear the tracking state when the timer
dismisses the banner or the key becomes invalid, so repeated updates for the
same key cannot reschedule dismissal.
In
`@examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart`:
- Around line 204-225: Update the host tile conditions in the available-hosts
mapping to use state.connectingHost, disabling the button and showing the
spinner only when the current host matches the in-flight host. Preserve the
existing enabled Connect state for all other hosts and keep the
_connect.connect(host) action unchanged.
In
`@examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel`+Generation.swift:
- Around line 76-90: In the hosted non-streaming branch of
generateStreamingResponse, inspect the aggregated result’s errorMessage before
calling updateMessageWithResult. If it is present, throw the corresponding
generation error so handleGenerationError runs, matching the existing error
handling in generateStreamingResponse’s streaming path.
In `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx`:
- Around line 799-816: Update the connect banner icon container styling in the
showConnectBanner block and its connectBannerIcon style so the badge background
matches connectState.status: use the success color for connected and the failure
color for disconnected or failed states, consistent with the icon glyph and
color.
- Around line 574-585: Update the hosted branch of the generateStream call in
ChatScreen so its history field uses the prior messages from
currentConversation.messages instead of an empty array. Leave the local
RunAnywhere.generateStream path unchanged and preserve the existing request
options.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/public/connect/connect_session.dart`:
- Around line 583-597: Update the generation read loop in generate() to apply a
generous timeout to each _readFrame() call, matching the existing bounded-read
behavior used by connect() and the heartbeat. Ensure timeout failures propagate
through the existing disconnect handling path, while preserving normal frame
validation and event yielding.
In
`@sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/connect/AndroidConnectTransport.kt`:
- Around line 280-291: Replace the broad Throwable catches with Exception
catches in generate, startHeartbeat, and connect, while keeping their existing
CancellationException branches first so cancellation is rethrown unchanged.
Preserve the current mapping and cleanup behavior for ordinary exceptions,
allowing JVM Error subclasses to propagate without being wrapped or swallowed.
In
`@sdk/runanywhere-react-native/packages/core/src/Public/Connect/ConnectSession.ts`:
- Around line 525-545: Update receiveData to append incoming Uint8Array bytes to
this.buffer without spreading data as function arguments, avoiding engine
argument-count limits for large TCP chunks. Preserve the existing frame parsing,
validation, waiter resolution, and buffering behavior; do not refactor the
buffer representation unless necessary for this fix.
In
`@sdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swift`:
- Around line 555-578: The connect flow in ConnectSession.connect lacks a
bounded timeout and can remain connecting indefinitely. Add a 5-second timeout
for each ClientAttempt, ensure it resumes the continuation with the appropriate
connection/timeout error only if still pending, and cancel or invalidate the
timeout when handleClientState completes the attempt.
---
Nitpick comments:
In `@examples/flutter/RunAnywhereAI/lib/app/content_view.dart`:
- Around line 30-46: Replace manual ConnectService.shared listener registration
and bare setState usage in
examples/flutter/RunAnywhereAI/lib/app/content_view.dart#L30-L46 with a
ListenableBuilder around _ConnectBanner/overlay consumption of _connect.state,
removing _connectChanged and its lifecycle hooks. In
examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart#L81-L99,
likewise wrap _buildConnectSection’s _connect.state consumption in
ListenableBuilder and remove manual listener wiring; preserve the post-frame
Navigator.pop behavior through a listener callback if required.
In `@examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart`:
- Around line 492-549: Extract the shared token aggregation, analytics
construction, persistence, and error-state handling from _generateStreaming and
_generateHostedStreaming into a private helper parameterized by the
Stream<LLMStreamEvent> source. Update both methods to supply their existing
stream sources and preserve their current prompt/options behavior, message
updates, and hosted-generation error handling.
In
`@examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Models/ModelSelectionSheet.swift`:
- Around line 374-398: Update connectRowLabel to replace the inline icon size,
frame dimensions, corner radius, and VStack spacing with the appropriate
existing AppTypography, AppLayout, and AppSpacing tokens. Reuse centralized
constants already defined for comparable model-selection rows, preserving the
current appearance and layout.
In `@examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx`:
- Around line 987-1007: Update the connectBanner style to replace the raw
backgroundColor and shadowColor literals with the appropriate shared Colors
theme tokens, matching the existing theme usage in ChatScreen.tsx and preserving
the banner’s visual intent across supported themes.
In
`@sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_connect.dart`:
- Around line 48-61: Update DartBridgeConnect._call to stop resolving symbols
through _library.lookupFunction on each request. Add and cache the Connect FFI
function pointers in native_functions.dart, following the existing native bridge
binding pattern, then have _call reuse the cached binding associated with each
symbol while preserving request invocation and decoding behavior.
🪄 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: d2341038-73aa-4590-beb0-ec81b9aec65b
⛔ Files ignored due to path filters (37)
sdk/runanywhere-commons/src/generated/proto/connect.pb.ccis excluded by!**/generated/**sdk/runanywhere-commons/src/generated/proto/connect.pb.his excluded by!**/generated/**sdk/runanywhere-flutter/packages/runanywhere/lib/generated/connect.pb.dartis excluded by!**/generated/**sdk/runanywhere-flutter/packages/runanywhere/lib/generated/connect.pbenum.dartis excluded by!**/generated/**sdk/runanywhere-flutter/packages/runanywhere/lib/generated/convenience/ra_convenience.dartis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectClientFrame.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectClientHello.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectClientSessionState.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectClientStartRequest.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectDiscoveryMetadata.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHandshakeResponse.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHandshakeStatus.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHeartbeatRequest.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHeartbeatResponse.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHostFrame.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHostStartRequest.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHostState.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHostStopRequest.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectInvocationEvent.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectInvocationRequest.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectInvocationValidation.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectModelDescriptor.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectPlatform.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectPlatformPolicy.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectPlatformPolicyRequest.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectRoleAvailability.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectSessionCloseRequest.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectSessionState.ktis excluded by!**/generated/**sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/convenience/RAConvenience.ktis 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/**sdk/runanywhere-react-native/yarn.lockis excluded by!**/yarn.lock,!**/*.locksdk/runanywhere-swift/Sources/RunAnywhere/Generated/RAConvenience.swiftis excluded by!**/generated/**sdk/runanywhere-swift/Sources/RunAnywhere/Generated/connect.pb.swiftis excluded by!**/generated/**sdk/shared/proto-ts/dist/connect.d.tsis excluded by!**/dist/**sdk/shared/proto-ts/dist/connect.jsis excluded by!**/dist/**yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (67)
examples/android/RunAnywhereAI/app/src/main/AndroidManifest.xmlexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/connect/ConnectClientViewModel.ktexamples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/connect/ConnectStatusBanner.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/models/ModelSelectionSheet.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/theme/icons/RACIcons.ktexamples/flutter/RunAnywhereAI/android/app/src/main/AndroidManifest.xmlexamples/flutter/RunAnywhereAI/ios/Runner/Info.plistexamples/flutter/RunAnywhereAI/lib/app/content_view.dartexamples/flutter/RunAnywhereAI/lib/core/services/connect_service.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dartexamples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dartexamples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dartexamples/ios/RunAnywhereAI/RunAnywhereAI/App/ConsumerAdvancedHubView.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+Events.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+Generation.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+ModelManagement.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Views/ChatInterfaceView.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Connect/ConnectClientController.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Connect/ConnectHostManagementView.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Connect/ConnectStatusBanner.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Features/Models/ModelSelectionSheet.swiftexamples/ios/RunAnywhereAI/RunAnywhereAI/Info.plistexamples/react-native/RunAnywhereAI/android/app/src/main/AndroidManifest.xmlexamples/react-native/RunAnywhereAI/ios/RunAnywhereAI/Info.plistexamples/react-native/RunAnywhereAI/package.jsonexamples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsxexamples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsxexamples/react-native/RunAnywhereAI/src/services/ConnectService.tsexamples/react-native/RunAnywhereAI/src/theme/system/icons.tsxidl/connect.protosdk/runanywhere-commons/CMakeLists.txtsdk/runanywhere-commons/exports/RACommons.exportssdk/runanywhere-commons/include/rac/connect/rac_connect.hsdk/runanywhere-commons/include/rac/connect/rac_connect_transport.hsdk/runanywhere-commons/src/connect/rac_connect.cppsdk/runanywhere-commons/src/connect/rac_connect_transport.cppsdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cppsdk/runanywhere-commons/tests/CMakeLists.txtsdk/runanywhere-commons/tests/test_connect_proto_abi.cppsdk/runanywhere-flutter/packages/runanywhere/android/src/main/AndroidManifest.xmlsdk/runanywhere-flutter/packages/runanywhere/android/src/main/kotlin/ai/runanywhere/sdk/RunAnywherePlugin.ktsdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_connect.dartsdk/runanywhere-flutter/packages/runanywhere/lib/public/connect/connect_session.dartsdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere.dartsdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere_protos.dartsdk/runanywhere-flutter/packages/runanywhere/pubspec.yamlsdk/runanywhere-kotlin/src/main/AndroidManifest.xmlsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeConnect.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/connect/AndroidConnectTransport.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.ktsdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/connect/ConnectSession.ktsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Connect.cppsdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.hppsdk/runanywhere-react-native/packages/core/package.jsonsdk/runanywhere-react-native/packages/core/src/Public/Connect/ConnectSession.tssdk/runanywhere-react-native/packages/core/src/index.tssdk/runanywhere-react-native/packages/core/src/specs/RunAnywhereCore.nitro.tssdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.hsdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_connect.hsdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Connect.swiftsdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swiftsdk/shared/proto-ts/src/connect.ts
| void didUpdateWidget(covariant _ConnectBanner oldWidget) { | ||
| super.didUpdateWidget(oldWidget); | ||
| final key = _key; | ||
| if (key == null || key == _dismissedKey) return; | ||
| _dismissedKey = null; | ||
| if (widget.state.status == ConnectStatus.connected) { | ||
| Future<void>.delayed(const Duration(seconds: 6), () { | ||
| if (mounted && _key == key) setState(() => _dismissedKey = key); | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Auto-dismiss timer can be re-scheduled repeatedly without a "already scheduled" guard.
didUpdateWidget resets _dismissedKey = null and re-schedules a new 6s Future.delayed every time it runs with an unchanged, non-dismissed connected key — there's no tracking of "already scheduled for this key". If ConnectService emits more than one state update for the same connected host/model within the 6s window (e.g. a heartbeat-driven snapshot), each call spawns another timer, and repeated emissions can perpetually push out the effective dismissal.
🛠️ Proposed fix: dedup scheduling per key
class _ConnectBannerState extends State<_ConnectBanner> {
String? _dismissedKey;
+ String? _scheduledKey;
...
`@override`
void didUpdateWidget(covariant _ConnectBanner oldWidget) {
super.didUpdateWidget(oldWidget);
final key = _key;
if (key == null || key == _dismissedKey) return;
- _dismissedKey = null;
- if (widget.state.status == ConnectStatus.connected) {
+ if (key != _dismissedKey) _dismissedKey = null;
+ if (widget.state.status == ConnectStatus.connected && key != _scheduledKey) {
+ _scheduledKey = key;
Future<void>.delayed(const Duration(seconds: 6), () {
if (mounted && _key == key) setState(() => _dismissedKey = key);
});
}
}🤖 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` around lines 184 -
194, Update _ConnectBanner.didUpdateWidget to track the key currently associated
with a scheduled auto-dismiss timer, and only schedule the 6-second
Future.delayed once per connected, non-dismissed key. Preserve the existing
key-change reset behavior and clear the tracking state when the timer dismisses
the banner or the key becomes invalid, so repeated updates for the same key
cannot reschedule dismissal.
| ...state.availableHosts.map( | ||
| (host) => ListTile( | ||
| leading: const Icon(Icons.desktop_windows_outlined), | ||
| title: Text( | ||
| host.displayName, | ||
| maxLines: 1, | ||
| overflow: TextOverflow.ellipsis, | ||
| ), | ||
| subtitle: const Text('Hosted language model available'), | ||
| trailing: FilledButton.tonal( | ||
| onPressed: state.status == ConnectStatus.connecting | ||
| ? null | ||
| : () => unawaited(_connect.connect(host)), | ||
| child: state.status == ConnectStatus.connecting | ||
| ? const SizedBox.square( | ||
| dimension: 18, | ||
| child: CircularProgressIndicator(strokeWidth: 2), | ||
| ) | ||
| : const Text('Connect'), | ||
| ), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
All discovered hosts show "connecting" simultaneously; state.connectingHost is unused.
Every host tile's onPressed/spinner condition checks the global state.status == ConnectStatus.connecting, so when the user taps "Connect" on one host, all discovered host tiles disable their button and show a spinner — not just the one actually being connected to. ConnectState.connectingHost exists specifically to identify which host is in-flight.
🛠️ Proposed fix: compare against the specific host
subtitle: const Text('Hosted language model available'),
trailing: FilledButton.tonal(
- onPressed: state.status == ConnectStatus.connecting
+ onPressed: state.connectingHost?.id == host.id
? null
: () => unawaited(_connect.connect(host)),
- child: state.status == ConnectStatus.connecting
+ child: state.connectingHost?.id == host.id
? const SizedBox.square(
dimension: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Connect'),
),📝 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.
| ...state.availableHosts.map( | |
| (host) => ListTile( | |
| leading: const Icon(Icons.desktop_windows_outlined), | |
| title: Text( | |
| host.displayName, | |
| maxLines: 1, | |
| overflow: TextOverflow.ellipsis, | |
| ), | |
| subtitle: const Text('Hosted language model available'), | |
| trailing: FilledButton.tonal( | |
| onPressed: state.status == ConnectStatus.connecting | |
| ? null | |
| : () => unawaited(_connect.connect(host)), | |
| child: state.status == ConnectStatus.connecting | |
| ? const SizedBox.square( | |
| dimension: 18, | |
| child: CircularProgressIndicator(strokeWidth: 2), | |
| ) | |
| : const Text('Connect'), | |
| ), | |
| ), | |
| ), | |
| ...state.availableHosts.map( | |
| (host) => ListTile( | |
| leading: const Icon(Icons.desktop_windows_outlined), | |
| title: Text( | |
| host.displayName, | |
| maxLines: 1, | |
| overflow: TextOverflow.ellipsis, | |
| ), | |
| subtitle: const Text('Hosted language model available'), | |
| trailing: FilledButton.tonal( | |
| onPressed: state.connectingHost?.id == host.id | |
| ? null | |
| : () => unawaited(_connect.connect(host)), | |
| child: state.connectingHost?.id == host.id | |
| ? const SizedBox.square( | |
| dimension: 18, | |
| child: CircularProgressIndicator(strokeWidth: 2), | |
| ) | |
| : const Text('Connect'), | |
| ), | |
| ), | |
| ), |
🤖 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/models/model_selection_sheet.dart`
around lines 204 - 225, Update the host tile conditions in the available-hosts
mapping to use state.connectingHost, disabling the button and showing the
spinner only when the current host matches the in-flight host. Preserve the
existing enabled Connect state for all other hosts and keep the
_connect.connect(host) action unchanged.
| let result: RALLMGenerationResult | ||
| #if os(iOS) | ||
| if isUsingConnect { | ||
| // Connect uses one typed streaming transport for both chat modes; | ||
| // aggregate it here when the user has disabled live token updates. | ||
| result = await RunAnywhere.aggregateStream( | ||
| prompt: prompt, | ||
| events: try await generationStream(for: request) | ||
| ) | ||
| } else { | ||
| result = try await RunAnywhere.generate(request) | ||
| } | ||
| #else | ||
| result = try await RunAnywhere.generate(request) | ||
| #endif |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Hosted non-streaming path never checks result.errorMessage, silently swallowing generation failures.
generateStreamingResponse (same file, lines 50-54) checks result.errorMessage after RunAnywhere.aggregateStream(...) and throws to trigger handleGenerationError. This new hosted non-streaming branch calls the same API but skips that check, so a hosted generation failure reported only via result.errorMessage will fall through to updateMessageWithResult and render as a normal (likely empty) assistant message instead of surfacing an error.
🐛 Proposed fix: check `errorMessage` before continuing
`#if` os(iOS)
if isUsingConnect {
// Connect uses one typed streaming transport for both chat modes;
// aggregate it here when the user has disabled live token updates.
result = await RunAnywhere.aggregateStream(
prompt: prompt,
events: try await generationStream(for: request)
)
+ if !result.errorMessage.isEmpty {
+ throw NSError(domain: "RunAnywhereAI", code: -1, userInfo: [
+ NSLocalizedDescriptionKey: result.errorMessage
+ ])
+ }
} else {
result = try await RunAnywhere.generate(request)
}
`#else`📝 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.
| let result: RALLMGenerationResult | |
| #if os(iOS) | |
| if isUsingConnect { | |
| // Connect uses one typed streaming transport for both chat modes; | |
| // aggregate it here when the user has disabled live token updates. | |
| result = await RunAnywhere.aggregateStream( | |
| prompt: prompt, | |
| events: try await generationStream(for: request) | |
| ) | |
| } else { | |
| result = try await RunAnywhere.generate(request) | |
| } | |
| #else | |
| result = try await RunAnywhere.generate(request) | |
| #endif | |
| let result: RALLMGenerationResult | |
| `#if` os(iOS) | |
| if isUsingConnect { | |
| // Connect uses one typed streaming transport for both chat modes; | |
| // aggregate it here when the user has disabled live token updates. | |
| result = await RunAnywhere.aggregateStream( | |
| prompt: prompt, | |
| events: try await generationStream(for: request) | |
| ) | |
| if !result.errorMessage.isEmpty { | |
| throw NSError(domain: "RunAnywhereAI", code: -1, userInfo: [ | |
| NSLocalizedDescriptionKey: result.errorMessage | |
| ]) | |
| } | |
| } else { | |
| result = try await RunAnywhere.generate(request) | |
| } | |
| `#else` | |
| result = try await RunAnywhere.generate(request) | |
| `#endif` |
🤖 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/Chat/ViewModels/LLMViewModel`+Generation.swift
around lines 76 - 90, In the hosted non-streaming branch of
generateStreamingResponse, inspect the aggregated result’s errorMessage before
calling updateMessageWithResult. If it is present, throw the corresponding
generation error so handleGenerationError runs, matching the existing error
handling in generateStreamingResponse’s streaming path.
| const eventStream = hostedModel | ||
| ? ConnectService.session.generateStream({ | ||
| prompt, | ||
| emitThoughts: options.thinkingModeEnabled, | ||
| requestId: assistantMessageId, | ||
| modelId: hostedModel.id, | ||
| conversationId: currentConversation.id, | ||
| metadata: {}, | ||
| options: genOptions, | ||
| history: [], | ||
| }) | ||
| : RunAnywhere.generateStream(prompt, genOptions); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== ChatScreen references ==\n'
rg -n "generateStream\\(|history: \\[\\]|conversationId|ConnectService\\.session" examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx
printf '\n== Locate ConnectService/session implementation ==\n'
fd -a "ConnectService" examples/react-native/RunAnywhereAI sdk . || true
fd -a "session" examples/react-native/RunAnywhereAI sdk . | head -n 50 || true
printf '\n== Search for generateStream signatures/usages ==\n'
rg -n "generateStream\\s*\\(" examples/react-native/RunAnywhereAI sdk -g '!**/node_modules/**'
printf '\n== Search for history contract ==\n'
rg -n "\\bhistory\\b" examples/react-native/RunAnywhereAI sdk -g '!**/node_modules/**' | head -n 300Repository: RunanywhereAI/runanywhere-sdks
Length of output: 50386
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== File list =="
git ls-files | rg 'examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx|ConnectService|session|history|protocol|generateStream'
echo
echo "== ChatScreen outline =="
ast-grep outline examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx --view expanded || true
echo
echo "== Find ConnectService/session implementation candidates =="
fd -a "ConnectService" . || true
fd -a "connect" examples/react-native/RunAnywhereAI sdk . | head -n 100 || true
fd -a "session" examples/react-native/RunAnywhereAI sdk . | head -n 100 || true
echo
echo "== Search for generateStream signature definitions =="
rg -n "generateStream\\s*[:(]" examples/react-native/RunAnywhereAI sdk -g '!**/node_modules/**' || true
echo
echo "== Search for history semantics in repo docs/types =="
rg -n "\\bhistory\\b" examples/react-native/RunAnywhereAI sdk docs . -g '!**/node_modules/**' | head -n 300 || trueRepository: RunanywhereAI/runanywhere-sdks
Length of output: 50386
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Read ChatScreen slice around the call =="
sed -n '520,620p' examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx | cat -n
echo
echo "== Locate ConnectService declaration and session type =="
rg -n "class ConnectService|const ConnectService|interface .*session|type .*session|generateStream\\(" examples/react-native/RunAnywhereAI sdk -g '!**/node_modules/**' -A 4 -B 4 || true
echo
echo "== Search for any comments/docs about history or conversationId =="
rg -n "conversationId|history" examples/react-native/RunAnywhereAI sdk -g '!**/node_modules/**' -A 2 -B 2 || trueRepository: RunanywhereAI/runanywhere-sdks
Length of output: 50389
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== React Native ConnectService =="
sed -n '1,260p' examples/react-native/RunAnywhereAI/src/services/ConnectService.ts | cat -n
echo
echo "== ChatScreen around message state and call site =="
sed -n '120,240p' examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx | cat -n
echo
sed -n '540,610p' examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx | cat -n
echo
echo "== React Native ConnectSession signature =="
sed -n '250,360p' sdk/runanywhere-react-native/packages/core/src/Public/Connect/ConnectSession.ts | cat -n
echo
echo "== iOS Connect session / controller search =="
rg -n "history|generateStream|conversationId|ConnectSession|ConnectClientController" examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Connect sdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect -A 3 -B 3 || trueRepository: RunanywhereAI/runanywhere-sdks
Length of output: 24397
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== ChatScreen send/generation path =="
rg -n "addMessage\\(|updateMessage\\(|generateStream\\(|assistantMessageId|role: MessageRole\\.User|role: MessageRole\\.Assistant|currentConversation\\.messages|messages =" \
examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx -A 6 -B 6
echo
echo "== Connect protocol request shape =="
sed -n '1,220p' sdk/shared/proto-ts/src/llm_service.ts | cat -n
echo
echo "== RN ConnectSession generateStream transport path =="
sed -n '1,220p' sdk/runanywhere-react-native/packages/core/src/Public/Connect/ConnectSession.ts | cat -nRepository: RunanywhereAI/runanywhere-sdks
Length of output: 24418
Hosted generateStream needs prior turns (examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx:575-584). history: [] strips conversation context from every Connect request; pass the prior messages from currentConversation.messages instead so hosted sessions keep multi-turn 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/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx` around lines
574 - 585, Update the hosted branch of the generateStream call in ChatScreen so
its history field uses the prior messages from currentConversation.messages
instead of an empty array. Leave the local RunAnywhere.generateStream path
unchanged and preserve the existing request options.
| {showConnectBanner && ( | ||
| <View style={[styles.connectBanner, { top: insets.top + 64 }]}> | ||
| <View style={styles.connectBannerIcon}> | ||
| <Icon | ||
| name={ | ||
| connectState.status === 'connected' | ||
| ? 'checkmark-circle' | ||
| : 'alert-circle-outline' | ||
| } | ||
| size={24} | ||
| color={ | ||
| connectState.status === 'connected' | ||
| ? Colors.statusGreen | ||
| : Colors.primaryRed | ||
| } | ||
| /> | ||
| </View> | ||
| <View style={styles.connectBannerText}> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Connect banner icon badge doesn't reflect failure state.
The icon glyph/color switches between green (connected) and red (disconnected/failed), but its background circle (connectBannerIcon, Line 1014) is hardcoded to Colors.badgeGreen regardless of status — producing a red alert icon on a green "success" badge when disconnected/failed, which miscommunicates connection health.
🐛 Proposed fix
- <View style={styles.connectBannerIcon}>
+ <View
+ style={[
+ styles.connectBannerIcon,
+ connectState.status !== 'connected' && styles.connectBannerIconAlert,
+ ]}
+ > connectBannerIcon: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Colors.badgeGreen,
},
+ connectBannerIconAlert: {
+ backgroundColor: Colors.badgeRed, // use whichever red-tinted token exists
+ },📝 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.
| {showConnectBanner && ( | |
| <View style={[styles.connectBanner, { top: insets.top + 64 }]}> | |
| <View style={styles.connectBannerIcon}> | |
| <Icon | |
| name={ | |
| connectState.status === 'connected' | |
| ? 'checkmark-circle' | |
| : 'alert-circle-outline' | |
| } | |
| size={24} | |
| color={ | |
| connectState.status === 'connected' | |
| ? Colors.statusGreen | |
| : Colors.primaryRed | |
| } | |
| /> | |
| </View> | |
| <View style={styles.connectBannerText}> | |
| {showConnectBanner && ( | |
| <View | |
| style={[ | |
| styles.connectBanner, | |
| { top: insets.top + 64 }, | |
| ]} | |
| > | |
| <View | |
| style={[ | |
| styles.connectBannerIcon, | |
| connectState.status !== 'connected' && styles.connectBannerIconAlert, | |
| ]} | |
| > | |
| <Icon | |
| name={ | |
| connectState.status === 'connected' | |
| ? 'checkmark-circle' | |
| : 'alert-circle-outline' | |
| } | |
| size={24} | |
| color={ | |
| connectState.status === 'connected' | |
| ? Colors.statusGreen | |
| : Colors.primaryRed | |
| } | |
| /> | |
| </View> | |
| <View style={styles.connectBannerText}> | |
| ... | |
| connectBannerIcon: { | |
| width: 36, | |
| height: 36, | |
| borderRadius: 18, | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| backgroundColor: Colors.badgeGreen, | |
| }, | |
| connectBannerIconAlert: { | |
| backgroundColor: Colors.badgeRed, | |
| }, |
🤖 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
799 - 816, Update the connect banner icon container styling in the
showConnectBanner block and its connectBannerIcon style so the badge background
matches connectState.status: use the success color for connected and the failure
color for disconnected or failed states, consistent with the icon glyph and
color.
| while (true) { | ||
| final frame = connect_pb.ConnectHostFrame.fromBuffer( | ||
| await _readFrame(), | ||
| ); | ||
| if (!frame.hasInvocationEvent() || | ||
| frame.invocationEvent.requestId != request.requestId || | ||
| !frame.invocationEvent.hasEvent()) { | ||
| throw SDKException.networkError( | ||
| 'The Connect host returned an invalid response', | ||
| ); | ||
| } | ||
| final event = frame.invocationEvent.event; | ||
| yield event; | ||
| if (event.isFinal) break; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Generation read loop has no timeout while heartbeat liveness is suppressed.
connect() (Line 519) and the heartbeat (Line 547) both bound their reads, but the generation loop calls await _readFrame() with no timeout. Because generate() holds _operationActive = true for its entire duration, the heartbeat at Line 533 is skipped throughout — so a host that stalls mid-stream (connection alive, no further frames) will hang the client indefinitely with no disconnect detection or failure surfaced to the caller. Add a generous inter-frame timeout so a stalled host is caught and routed through _handleDisconnect.
🛡️ Bound the per-frame read on the generation path
while (true) {
final frame = connect_pb.ConnectHostFrame.fromBuffer(
- await _readFrame(),
+ await _readFrame().timeout(const Duration(seconds: 30)),
);📝 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.
| while (true) { | |
| final frame = connect_pb.ConnectHostFrame.fromBuffer( | |
| await _readFrame(), | |
| ); | |
| if (!frame.hasInvocationEvent() || | |
| frame.invocationEvent.requestId != request.requestId || | |
| !frame.invocationEvent.hasEvent()) { | |
| throw SDKException.networkError( | |
| 'The Connect host returned an invalid response', | |
| ); | |
| } | |
| final event = frame.invocationEvent.event; | |
| yield event; | |
| if (event.isFinal) break; | |
| } | |
| while (true) { | |
| final frame = connect_pb.ConnectHostFrame.fromBuffer( | |
| await _readFrame().timeout(const Duration(seconds: 30)), | |
| ); | |
| if (!frame.hasInvocationEvent() || | |
| frame.invocationEvent.requestId != request.requestId || | |
| !frame.invocationEvent.hasEvent()) { | |
| throw SDKException.networkError( | |
| 'The Connect host returned an invalid response', | |
| ); | |
| } | |
| final event = frame.invocationEvent.event; | |
| yield event; | |
| if (event.isFinal) break; | |
| } |
🤖 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/connect/connect_session.dart`
around lines 583 - 597, Update the generation read loop in generate() to apply a
generous timeout to each _readFrame() call, matching the existing bounded-read
behavior used by connect() and the heartbeat. Ensure timeout failures propagate
through the existing disconnect handling path, while preserving normal frame
validation and event yielding.
| } catch (error: CancellationException) { | ||
| // A partially consumed framed stream cannot be reused safely. | ||
| close(notify = true, reason = error) | ||
| throw error | ||
| } catch (error: Throwable) { | ||
| val mapped = mapNetworkError("Generation through the host failed", error) | ||
| close(notify = true, reason = mapped) | ||
| throw mapped | ||
| } finally { | ||
| operationMutex.unlock() | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Catch Exception, not Throwable, so JVM Errors propagate.
generate catches Throwable and routes it through mapNetworkError, whose else branch wraps any throwable — including java.lang.Error subclasses (OOM, StackOverflow) — into an SDKException.networkError, masking fatal JVM conditions. The same pattern appears in startHeartbeat (Line 236, which additionally swallows it via close()/return@launch) and connect (Line 197). After the CancellationException branch, narrow these to catch (error: Exception).
Based on learnings: always rethrow kotlinx.coroutines.CancellationException and use a broad catch (e: Exception) rather than catching Throwable/Error; allow JVM Error subclasses to propagate.
🤖 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/connect/AndroidConnectTransport.kt`
around lines 280 - 291, Replace the broad Throwable catches with Exception
catches in generate, startHeartbeat, and connect, while keeping their existing
CancellationException branches first so cancellation is rethrown unchanged.
Preserve the current mapping and cleanup behavior for ordinary exceptions,
allowing JVM Error subclasses to propagate without being wrapped or swallowed.
Source: Learnings
| private receiveData(data: Uint8Array | string): void { | ||
| if (typeof data === 'string') return; | ||
| this.buffer.push(...data); | ||
| while (this.buffer.length >= 4) { | ||
| const length = | ||
| ((this.buffer[0] << 24) >>> 0) | | ||
| (this.buffer[1] << 16) | | ||
| (this.buffer[2] << 8) | | ||
| this.buffer[3]; | ||
| if (length < 1 || length > MAX_FRAME_LENGTH) { | ||
| this.fail(new Error('The Connect host returned an invalid frame size')); | ||
| return; | ||
| } | ||
| if (this.buffer.length < length + 4) return; | ||
| const frame = Uint8Array.from(this.buffer.slice(4, length + 4)); | ||
| this.buffer.splice(0, length + 4); | ||
| const waiter = this.waiters.shift(); | ||
| if (waiter) waiter.resolve(frame); | ||
| else this.frames.push(frame); | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Spreading incoming TCP data into push(...data) can crash on large chunks.
this.buffer.push(...data) spreads every byte of the incoming Uint8Array into function arguments. TCP can coalesce/deliver large chunks (frames are allowed up to MAX_FRAME_LENGTH = 4 MB), and spreading a large typed array exceeds the engine's argument-count limit, throwing RangeError: Maximum call stack size exceeded on the receive path — tearing down an otherwise healthy session. Append without spreading.
🛡️ Minimal fix (avoid arg-count crash)
private receiveData(data: Uint8Array | string): void {
if (typeof data === 'string') return;
- this.buffer.push(...data);
+ for (let i = 0; i < data.length; i++) this.buffer.push(data[i]);
while (this.buffer.length >= 4) {Note: the number[] buffer with repeated slice/splice is also O(n) per frame; consider accumulating Uint8Array chunks and slicing on offsets if the receive path becomes hot.
📝 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.
| private receiveData(data: Uint8Array | string): void { | |
| if (typeof data === 'string') return; | |
| this.buffer.push(...data); | |
| while (this.buffer.length >= 4) { | |
| const length = | |
| ((this.buffer[0] << 24) >>> 0) | | |
| (this.buffer[1] << 16) | | |
| (this.buffer[2] << 8) | | |
| this.buffer[3]; | |
| if (length < 1 || length > MAX_FRAME_LENGTH) { | |
| this.fail(new Error('The Connect host returned an invalid frame size')); | |
| return; | |
| } | |
| if (this.buffer.length < length + 4) return; | |
| const frame = Uint8Array.from(this.buffer.slice(4, length + 4)); | |
| this.buffer.splice(0, length + 4); | |
| const waiter = this.waiters.shift(); | |
| if (waiter) waiter.resolve(frame); | |
| else this.frames.push(frame); | |
| } | |
| } | |
| private receiveData(data: Uint8Array | string): void { | |
| if (typeof data === 'string') return; | |
| for (let i = 0; i < data.length; i++) this.buffer.push(data[i]); | |
| while (this.buffer.length >= 4) { | |
| const length = | |
| ((this.buffer[0] << 24) >>> 0) | | |
| (this.buffer[1] << 16) | | |
| (this.buffer[2] << 8) | | |
| this.buffer[3]; | |
| if (length < 1 || length > MAX_FRAME_LENGTH) { | |
| this.fail(new Error('The Connect host returned an invalid frame size')); | |
| return; | |
| } | |
| if (this.buffer.length < length + 4) return; | |
| const frame = Uint8Array.from(this.buffer.slice(4, length + 4)); | |
| this.buffer.splice(0, length + 4); | |
| const waiter = this.waiters.shift(); | |
| if (waiter) waiter.resolve(frame); | |
| else this.frames.push(frame); | |
| } | |
| } |
🤖 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/Connect/ConnectSession.ts`
around lines 525 - 545, Update receiveData to append incoming Uint8Array bytes
to this.buffer without spreading data as function arguments, avoiding engine
argument-count limits for large TCP chunks. Preserve the existing frame parsing,
validation, waiter resolution, and buffering behavior; do not refactor the
buffer representation unless necessary for this fix.
| func connect( | ||
| to host: ConnectHost, | ||
| hello: RAConnectClientHello | ||
| ) async throws -> RAConnectHandshakeResponse { | ||
| try await withCheckedThrowingContinuation { continuation in | ||
| queue.async { [weak self] in | ||
| guard let self, | ||
| let endpoint = self.discoveredEndpoints[host.id] else { | ||
| continuation.resume(throwing: ConnectTransportError.hostUnavailable) | ||
| return | ||
| } | ||
|
|
||
| self.clearClientConnection(notify: false) | ||
| let connection = NWConnection(to: endpoint, using: .tcp) | ||
| let attempt = ClientAttempt(continuation: continuation) | ||
| connection.stateUpdateHandler = { [weak self, weak connection] state in | ||
| guard let self, let connection else { return } | ||
| self.handleClientState(state, connection: connection, hello: hello, attempt: attempt) | ||
| } | ||
| self.clientConnection = connection | ||
| connection.start(queue: self.queue) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
NWConnection default TCP connection establishment timeout Network.framework
💡 Result:
The Network.framework does not expose a single, documented, publicly guaranteed "default" timeout value for TCP connection establishment [1][2]. Instead, NWConnection relies on the underlying system-level TCP implementation, which generally employs long timeout periods—often in the 30 to 75-second range—to account for various network conditions [3]. Because the Network.framework is designed to be highly asynchronous and resilient, it prioritizes allowing the connection process to succeed over network path changes or transient issues, rather than aggressively timing out [1][2]. If you require a specific, shorter timeout for your application, you can explicitly configure it using the NWProtocolTCP.Options class [3][4]. By setting the connectionTimeout property, you control the maximum number of seconds the framework will wait for the TCP handshake to complete before returning an error [5][6][7]. Example configuration in Swift: let tcpOptions = NWProtocolTCP.Options tcpOptions.connectionTimeout = 5 // Set timeout to 5 seconds let parameters = NWParameters(tls: nil, tcp: tcpOptions) let connection = NWConnection(host: "example.com", port: 80, using: parameters) When this custom timeout is reached, the connection will typically transition to the.waiting state with a corresponding NWError (such as POSIXErrorCode.ETIMEDOUT) [3][4][1]. Setting a custom timeout is generally preferred over manually managing timers in your application code [3][4].
Citations:
- 1: https://developer.apple.com/forums/thread/774933
- 2: https://developer.apple.com/forums/thread/728130
- 3: https://developer.apple.com/forums/thread/662177
- 4: https://stackoverflow.com/questions/64102383/nwconnection-timeout
- 5: https://developer.apple.com/documentation/network/tcp-options
- 6: https://developer.apple.com/documentation/network/nwprotocoltcp/options/connectiontimeout
- 7: https://developer.apple.com/documentation/network/tcp/connectiontimeout(_:)
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant Swift file and look for any existing connect-timeout handling.
git ls-files sdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swift
ast-grep outline sdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swift --view expanded
printf '\n--- timeout-related symbols in ConnectSession.swift ---\n'
rg -n "timeout|connectionTimeout|NWProtocolTCP|NWParameters|DispatchWorkItem|cancel\(" sdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swift
printf '\n--- nearby connect implementation context ---\n'
sed -n '520,640p' sdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swiftRepository: RunanywhereAI/runanywhere-sdks
Length of output: 10317
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n--- client state handler ---\n'
sed -n '1080,1180p' sdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swift
printf '\n--- android connect timeout references ---\n'
rg -n "CONNECT_TIMEOUT_MS|connect timeout|connection timeout|timeout" sdk/runanywhere-kotlin sdk/runanywhere-android sdk -g '!**/build/**' -g '!**/node_modules/**'Repository: RunanywhereAI/runanywhere-sdks
Length of output: 50387
Add a bounded connect timeout here. Without one, an unreachable host can leave the session stuck in .connecting for a long time; the Android transport already caps this at 5s, so matching that would improve 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-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swift`
around lines 555 - 578, The connect flow in ConnectSession.connect lacks a
bounded timeout and can remain connecting indefinitely. Add a 5-second timeout
for each ClientAttempt, ensure it resumes the continuation with the appropriate
connection/timeout error only if still pending, and cancel or invalidate the
timeout when handleClientState completes the attempt.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
sdk/runanywhere-kotlin/modules/runanywhere-core-qhexrt/src/main/kotlin/com/runanywhere/sdk/npu/qhexrt/QHexRTSkelInstaller.kt (1)
39-46: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winKeep cached-install validation aligned with the expanded asset filter.
installIfAvailable()now acceptslibrun_main_on_hexagon_skel.so, butcontainsSkel()only checks forlibQnnHtpV*Skel.soat Lines 90-94. This can cause repeated extraction for bundles without an HTP skel, or return a cached directory that lacks the newly supported file. Reuse the same filename predicate, or validate that all selectedskelNamesare present before returning the cached path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdk/runanywhere-kotlin/modules/runanywhere-core-qhexrt/src/main/kotlin/com/runanywhere/sdk/npu/qhexrt/QHexRTSkelInstaller.kt` around lines 39 - 46, Update containsSkel() to use the same asset predicate as installIfAvailable(), including librun_main_on_hexagon_skel.so and other selected skelNames. Ensure cached-install validation confirms every selected filename is present before returning the cached path, preventing repeated extraction or incomplete cached installs.
🤖 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.
Outside diff comments:
In
`@sdk/runanywhere-kotlin/modules/runanywhere-core-qhexrt/src/main/kotlin/com/runanywhere/sdk/npu/qhexrt/QHexRTSkelInstaller.kt`:
- Around line 39-46: Update containsSkel() to use the same asset predicate as
installIfAvailable(), including librun_main_on_hexagon_skel.so and other
selected skelNames. Ensure cached-install validation confirms every selected
filename is present before returning the cached path, preventing repeated
extraction or incomplete cached installs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a9ae5316-cdd4-48ed-bf7c-1804c1dc494b
📒 Files selected for processing (1)
sdk/runanywhere-kotlin/modules/runanywhere-core-qhexrt/src/main/kotlin/com/runanywhere/sdk/npu/qhexrt/QHexRTSkelInstaller.kt
What
Scope
The host is macOS-only in this phase. Mobile clients operate on the local network; WAN transport is intentionally out of scope. Web and future Windows support retain protocol and capability-policy foundations but do not enable functional clients or hosts in this PR.
The PR remains draft while physical Android-device interoperability is verified by the team.
Validation
Notes
Summary by CodeRabbit