Skip to content

feat(connect): add macOS host support for mobile clients#556

Open
yavuzkaanakyuz wants to merge 7 commits into
RunanywhereAI:mainfrom
yavuzkaanakyuz:host-feature
Open

feat(connect): add macOS host support for mobile clients#556
yavuzkaanakyuz wants to merge 7 commits into
RunanywhereAI:mainfrom
yavuzkaanakyuz:host-feature

Conversation

@yavuzkaanakyuz

@yavuzkaanakyuz yavuzkaanakyuz commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

What

  • Add the canonical Connect protocol, generated bindings, and platform-neutral C++ role/session/transport layer.
  • Add the macOS host implementation using Bonjour and Network.framework.
  • Add functional Connect clients for iPhone/iPad, native Android/Kotlin, Flutter, and React Native.
  • Route hosted generation through the selected host model with heartbeat-driven disconnect detection, local-model fallback, lazy local-network permission, and dynamic connection banners.

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

  • Commons proto/build/test validation, including Connect role, handshake, reconnect, and session-count coverage
  • Android SDK AAR build and Android example debug APK build
  • Kotlin SDK unit tests
  • Flutter SDK and example analysis
  • React Native core/example typecheck and lint; Android native core build
  • Swift RunAnywhere target build and strict SwiftLint
  • Generated IDL drift and repository diff checks

Notes

  • The branch is merged with current main; the previous chat-generation conflict is resolved while preserving both Connect routing and the latest history/generation-ownership fixes.
  • Local Xcode signing/build overrides are excluded from the PR.

Summary by CodeRabbit

  • New Features
    • Added Connect support to discover and connect to hosted language models on nearby Macs/devices.
    • Chat can stream hosted-model responses and reflect “Ready on host” status.
    • Added connection status banners and model switching/connect/retry/disconnect controls across Android, iOS, Flutter, and React Native; macOS can host a loaded model for clients.
  • Bug Fixes
    • Improved connection lifecycle cleanup and reduced stale model/status display after disconnects.
  • Permissions
    • Added/updated local-network discovery permissions, Wi‑Fi/multicast requirements, and Bonjour/network privacy descriptions.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Connect protocol and native core

Layer / File(s) Summary
Protocol, ABI, transport registry, and tests
idl/connect.proto, sdk/runanywhere-commons/..., sdk/shared/proto-ts/src/connect.ts
Defines Connect policy, handshake, session, invocation, heartbeat, and frame messages; implements native validation and transport ABI functions; adds bridges, generated TypeScript types, build/export wiring, and ABI tests.

Platform SDKs

Layer / File(s) Summary
Swift Connect runtime
sdk/runanywhere-swift/...
Adds Apple Bonjour/TCP discovery, hosting, client sessions, framed streaming, heartbeats, cleanup, and typed C++ bridge wrappers.
Kotlin Connect runtime
sdk/runanywhere-kotlin/...
Adds Android NSD discovery, TCP framing, handshake validation, heartbeat monitoring, generation streaming, and public session state.
Flutter Connect runtime
sdk/runanywhere-flutter/...
Adds mDNS discovery, Android multicast-lock handling, TCP framing, heartbeat processing, hosted generation, Dart bridges, and public exports.
React Native Connect runtime
sdk/runanywhere-react-native/...
Adds native protobuf bridges, zeroconf/TCP sessions, heartbeat validation, streamed generation, dependencies, and public exports.

Example applications

Layer / File(s) Summary
Android hosted-model flow
examples/android/RunAnywhereAI/...
Adds permission-gated discovery, host selection, status banners, hosted generation, hosted-model presentation, and local-model disconnect behavior.
Flutter hosted-model flow
examples/flutter/RunAnywhereAI/...
Adds a shared Connect service, discovery and connection picker, connection banner, hosted generation, and local-network metadata.
iOS host and client flow
examples/ios/RunAnywhereAI/...
Adds macOS host management, iOS discovery and connection banners, hosted-model synchronization, hosted generation routing, and local-network metadata.
React Native hosted-model flow
examples/react-native/RunAnywhereAI/...
Adds Connect service state management, host selection, hosted streaming, connection banners, hosted-aware chat controls, icons, dependencies, and platform metadata.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement, core, android-sample, ios-sample, kotlin-sdk, flutter-sdk, rn-sdk

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
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description uses custom sections and omits the required template sections and checklists. Rewrite it to match the repository template and fill in the missing Type of Change, Testing, Labels, Checklist, and Screenshots sections.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title captures the main change: Connect-based macOS host support for mobile clients.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@sanchitmonga22

Copy link
Copy Markdown
Contributor

@coderabbitai please do a detailed review

@yavuzkaanakyuz yavuzkaanakyuz changed the title feat(connect): add macOS host support for iOS clients feat(connect): add macOS host support for mobile clients Jul 16, 2026
@yavuzkaanakyuz
yavuzkaanakyuz marked this pull request as ready for review July 19, 2026 19:15
@yavuzkaanakyuz

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (5)
sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_connect.dart (1)

48-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the Connect FFI lookups.
DartBridgeConnect resolves each rac_connect_* symbol on every request. Move these bindings into native_functions.dart so 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 win

Inline magic numbers in connectRowLabel should use centralized layout tokens.

size: 18, width: 38, height: 38, cornerRadius: 10, and spacing: 2 are inline magic numbers in a view. Route these through AppSpacing/AppLayout/AppTypography/AdaptiveSizing (e.g., an AppTypography icon weight, AppLayout icon frame + corner radius, AppSpacing for 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, and AdaptiveSizing; 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 win

Duplicate streaming-aggregation logic with _generateStreaming.

_generateHostedStreaming re-implements almost all of _generateStreaming's token-aggregation, analytics-building, and persistence logic, differing only in the event source (ConnectService.shared.session.generateStream vs sdk.RunAnywhere.llm.generateStream). Consider extracting a shared private helper parameterized by the Stream<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 win

Both sites consume the shared ConnectService.shared singleton via manual addListener/setState instead of ListenableBuilder. The documented state-management pattern for shared ChangeNotifier singletons is ListenableBuilder; both widgets instead wire up manual listener registration/removal plus bare setState(() {}).

  • examples/flutter/RunAnywhereAI/lib/app/content_view.dart#L30-L46: wrap the _ConnectBanner/overlay consumption of _connect.state in a ListenableBuilder(listenable: _connect, ...) and drop the manual addListener/removeListener/_connectChanged.
  • examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart#L81-L99: wrap _buildConnectSection's consumption of _connect.state in a ListenableBuilder(listenable: _connect, ...) instead of the manual listener + setState, keeping the post-frame Navigator.pop side 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 win

Raw color literals instead of theme tokens in connectBanner.

backgroundColor: 'rgba(255,255,255,0.96)' and shadowColor: '#000000' bypass Colors.*, 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, and spacing.ts where 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

📥 Commits

Reviewing files that changed from the base of the PR and between 541842a and cec0a0e.

⛔ Files ignored due to path filters (37)
  • sdk/runanywhere-commons/src/generated/proto/connect.pb.cc is excluded by !**/generated/**
  • sdk/runanywhere-commons/src/generated/proto/connect.pb.h is excluded by !**/generated/**
  • sdk/runanywhere-flutter/packages/runanywhere/lib/generated/connect.pb.dart is excluded by !**/generated/**
  • sdk/runanywhere-flutter/packages/runanywhere/lib/generated/connect.pbenum.dart is excluded by !**/generated/**
  • sdk/runanywhere-flutter/packages/runanywhere/lib/generated/convenience/ra_convenience.dart is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectClientFrame.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectClientHello.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectClientSessionState.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectClientStartRequest.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectDiscoveryMetadata.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHandshakeResponse.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHandshakeStatus.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHeartbeatRequest.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHeartbeatResponse.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHostFrame.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHostStartRequest.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHostState.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectHostStopRequest.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectInvocationEvent.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectInvocationRequest.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectInvocationValidation.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectModelDescriptor.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectPlatform.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectPlatformPolicy.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectPlatformPolicyRequest.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectRoleAvailability.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectSessionCloseRequest.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/ai/runanywhere/proto/v1/ConnectSessionState.kt is excluded by !**/generated/**
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/generated/convenience/RAConvenience.kt is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.cpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/packages/core/nitrogen/generated/shared/c++/HybridRunAnywhereCoreSpec.hpp is excluded by !**/generated/**
  • sdk/runanywhere-react-native/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • sdk/runanywhere-swift/Sources/RunAnywhere/Generated/RAConvenience.swift is excluded by !**/generated/**
  • sdk/runanywhere-swift/Sources/RunAnywhere/Generated/connect.pb.swift is excluded by !**/generated/**
  • sdk/shared/proto-ts/dist/connect.d.ts is excluded by !**/dist/**
  • sdk/shared/proto-ts/dist/connect.js is excluded by !**/dist/**
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (67)
  • examples/android/RunAnywhereAI/app/src/main/AndroidManifest.xml
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/connect/ConnectClientViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/connect/ConnectStatusBanner.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatTopBar.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/chat/ChatViewModel.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/models/ModelSelectionSheet.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/system_ui/AppScaffold.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/screens/system_ui/AppTopBar.kt
  • examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/ui/theme/icons/RACIcons.kt
  • examples/flutter/RunAnywhereAI/android/app/src/main/AndroidManifest.xml
  • examples/flutter/RunAnywhereAI/ios/Runner/Info.plist
  • examples/flutter/RunAnywhereAI/lib/app/content_view.dart
  • examples/flutter/RunAnywhereAI/lib/core/services/connect_service.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart
  • examples/flutter/RunAnywhereAI/lib/features/chat/chat_view_model.dart
  • examples/flutter/RunAnywhereAI/lib/features/models/model_selection_sheet.dart
  • examples/ios/RunAnywhereAI/RunAnywhereAI/App/ConsumerAdvancedHubView.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+Events.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+Generation.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel+ModelManagement.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/ViewModels/LLMViewModel.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Chat/Views/ChatInterfaceView.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Connect/ConnectClientController.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Connect/ConnectHostManagementView.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Connect/ConnectStatusBanner.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Features/Models/ModelSelectionSheet.swift
  • examples/ios/RunAnywhereAI/RunAnywhereAI/Info.plist
  • examples/react-native/RunAnywhereAI/android/app/src/main/AndroidManifest.xml
  • examples/react-native/RunAnywhereAI/ios/RunAnywhereAI/Info.plist
  • examples/react-native/RunAnywhereAI/package.json
  • examples/react-native/RunAnywhereAI/src/components/model/ModelSelectionSheet.tsx
  • examples/react-native/RunAnywhereAI/src/screens/ChatScreen.tsx
  • examples/react-native/RunAnywhereAI/src/services/ConnectService.ts
  • examples/react-native/RunAnywhereAI/src/theme/system/icons.tsx
  • idl/connect.proto
  • sdk/runanywhere-commons/CMakeLists.txt
  • sdk/runanywhere-commons/exports/RACommons.exports
  • sdk/runanywhere-commons/include/rac/connect/rac_connect.h
  • sdk/runanywhere-commons/include/rac/connect/rac_connect_transport.h
  • sdk/runanywhere-commons/src/connect/rac_connect.cpp
  • sdk/runanywhere-commons/src/connect/rac_connect_transport.cpp
  • sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp
  • sdk/runanywhere-commons/tests/CMakeLists.txt
  • sdk/runanywhere-commons/tests/test_connect_proto_abi.cpp
  • sdk/runanywhere-flutter/packages/runanywhere/android/src/main/AndroidManifest.xml
  • sdk/runanywhere-flutter/packages/runanywhere/android/src/main/kotlin/ai/runanywhere/sdk/RunAnywherePlugin.kt
  • sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_connect.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/public/connect/connect_session.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere.dart
  • sdk/runanywhere-flutter/packages/runanywhere/lib/runanywhere_protos.dart
  • sdk/runanywhere-flutter/packages/runanywhere/pubspec.yaml
  • sdk/runanywhere-kotlin/src/main/AndroidManifest.xml
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/bridge/extensions/CppBridgeConnect.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/connect/AndroidConnectTransport.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/native/bridge/RunAnywhereBridge.kt
  • sdk/runanywhere-kotlin/src/main/kotlin/com/runanywhere/sdk/public/connect/ConnectSession.kt
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore+Connect.cpp
  • sdk/runanywhere-react-native/packages/core/cpp/HybridRunAnywhereCore.hpp
  • sdk/runanywhere-react-native/packages/core/package.json
  • sdk/runanywhere-react-native/packages/core/src/Public/Connect/ConnectSession.ts
  • sdk/runanywhere-react-native/packages/core/src/index.ts
  • sdk/runanywhere-react-native/packages/core/src/specs/RunAnywhereCore.nitro.ts
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/CRACommons.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/CRACommons/include/rac_connect.h
  • sdk/runanywhere-swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+Connect.swift
  • sdk/runanywhere-swift/Sources/RunAnywhere/Public/Connect/ConnectSession.swift
  • sdk/shared/proto-ts/src/connect.ts

Comment on lines +184 to +194
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);
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +204 to +225
...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'),
),
),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 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.

Suggested change
...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.

Comment on lines +76 to +90
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 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.

Suggested change
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.

Comment on lines +574 to +585
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 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 300

Repository: 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 || true

Repository: 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 || true

Repository: 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 || true

Repository: 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 -n

Repository: 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.

Comment on lines +799 to +816
{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}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 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.

Suggested change
{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.

Comment on lines +583 to +597
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 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.

Suggested change
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.

Comment on lines +280 to +291
} 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()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

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

Comment on lines +525 to +545
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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 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.

Suggested change
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.

Comment on lines +555 to +578
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)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 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:


🏁 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.swift

Repository: 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Keep cached-install validation aligned with the expanded asset filter.

installIfAvailable() now accepts librun_main_on_hexagon_skel.so, but containsSkel() only checks for libQnnHtpV*Skel.so at 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 selected skelNames are 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

📥 Commits

Reviewing files that changed from the base of the PR and between cec0a0e and a1e98ac.

📒 Files selected for processing (1)
  • sdk/runanywhere-kotlin/modules/runanywhere-core-qhexrt/src/main/kotlin/com/runanywhere/sdk/npu/qhexrt/QHexRTSkelInstaller.kt

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants