Skip to content

feat: free-RAM hard gate before heavy model load#4

Merged
involvex merged 4 commits into
mainfrom
feat/ram-hard-gate
Jul 18, 2026
Merged

feat: free-RAM hard gate before heavy model load#4
involvex merged 4 commits into
mainfrom
feat/ram-hard-gate

Conversation

@involvex

@involvex involvex commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Read availMemMb from diagnostics channel
  • Block cold load of Gemma 4 (<~3500 MB free) and mid models (<~1200 MB free) on Android
  • Soft skip when free RAM cannot be read

Test plan

  • flutter test test/services/free_ram_gate_test.dart
  • On a low-RAM device, loading Gemma 4 shows a clear error instead of LMK
  • On Poco 12 GB with free RAM, Gemma 4 still loads

Summary by Sourcery

Add Android-specific RAM gating, app-opening, and sharing improvements to reduce crashes and improve device integration.

New Features:

  • Introduce an Android free-RAM gate that blocks cold loads of heavy models when available memory is below configurable thresholds.
  • Add deterministic open-app handling by parsing natural-language intent into known Android packages and executing an open_app tool call.
  • Enable partial (live) voice transcription updates in the input field while dictation is ongoing.
  • Support text-based exports of conversations and full chat history that can be shared via the platform share sheet instead of writing local files.

Bug Fixes:

  • Fix voice dictation truncating or leaving text stuck by deferring send until listening fully stops and avoiding auto-send while a response is generating.

Enhancements:

  • Extend alarm time parsing to handle German-language alarm phrases and Uhr-based time formats.
  • Augment model system prompts on Android to describe on-device tool capabilities and JSON-based function calling when native FC is unavailable.
  • Improve memory diagnostics by tracking and exposing available system RAM for use in load gating logic.
  • Clarify open_app tool metadata with English/German triggers and common Android package examples.

Documentation:

  • Update the feature plan to mark the free-RAM hard gate before Gemma 4 load as completed.

Tests:

  • Add unit tests for the free-RAM gate messaging thresholds on Android heavy and small models.
  • Add tests covering German alarm parsing, open-app intent resolution, and chat history text export behavior.

involvex and others added 3 commits July 18, 2026 03:51
Accumulate STT until listening ends so early finalResult no longer sends a partial message. Add open-app/alarm intent shortcuts and restore tool capability text when native FC is disabled on Android Gemma 4.

Co-authored-by: Cursor <cursoragent@cursor.com>
Normalize umlauts so geöffnet is not treated as an open intent, gate device-tool capability text to Android, and align beginner voice busy checks with the main chat screen.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stop writing nova_export files into the app-private documents path. Build export content in memory and open the existing Android share chooser so users can save to Files, Drive, or other apps.

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

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: b8a2d3b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@sourcery-ai

sourcery-ai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements an Android-only free-RAM hard gate before cold-loading heavy models, adds deterministic open-app handling and device-tool capability prompts, refines voice dictation behavior, switches chat export to in-memory text/JSON with a new sharing flow, and broadens German-language alarm and open-app parsing, with tests for the new behaviors.

Sequence diagram for Android free-RAM hard gate before heavy model load

sequenceDiagram
  participant AssistantScreen
  participant ModelOrchestrator
  participant PlatformAdaptationService
  participant MemoryDiagnosticsService
  AssistantScreen->>ModelOrchestrator: getOrLoadModel(model)
  ModelOrchestrator->>PlatformAdaptationService: checkCanLoadModel(model)
  PlatformAdaptationService->>MemoryDiagnosticsService: readAvailableMemMb()
  MemoryDiagnosticsService-->>PlatformAdaptationService: availMemMb
  PlatformAdaptationService-->>ModelOrchestrator: freeRamGateMessage or null
  alt RAM too low
    ModelOrchestrator-->>AssistantScreen: throw ModelException(message)
  else RAM sufficient or gate not applicable
    ModelOrchestrator-->>AssistantScreen: proceed to load/switch model
  end
Loading

Sequence diagram for deterministic open-app intent handling

sequenceDiagram
  actor User
  participant AssistantScreen
  participant ModelOrchestrator
  participant OpenAppIntentParser
  participant ToolExecutorService

  User->>AssistantScreen: enter query
  AssistantScreen->>ModelOrchestrator: sendMessage(query)
  ModelOrchestrator->>OpenAppIntentParser: tryParsePackage(query)
  OpenAppIntentParser-->>ModelOrchestrator: openPackage or null
  alt openPackage != null
    ModelOrchestrator->>ToolExecutorService: openApp(openPackage)
    ToolExecutorService-->>ModelOrchestrator: result{success,error}
    ModelOrchestrator-->>AssistantScreen: InferenceResult with open_app toolCalls
  else no open-app intent
    ModelOrchestrator-->>AssistantScreen: normal inference flow
  end
Loading

File-Level Changes

Change Details Files
Add Android free-RAM hard gate before cold-loading heavy models and expose thresholds/utilities for testing.
  • Extend MemoryDiagnosticsService to read and cache availMemMb from the platform diagnostics channel, alongside PSS/RSS.
  • Add PlatformAdaptationService.minFreeRamMbFor and freeRamGateMessage helpers to define per-model RAM thresholds (e.g., Gemma 4 and mid models) and generate user-facing error messages.
  • Introduce PlatformAdaptationService.checkCanLoadModel, and invoke it in ModelOrchestrator before loading a new model, throwing ModelException when RAM is insufficient.
  • Add unit tests (free_ram_gate_test.dart) to validate gate behavior across models and availMemMb scenarios and mark the plan item as done in PLAN-features.md.
lib/services/memory_diagnostics_service.dart
lib/services/platform_adaptation_service.dart
lib/services/model_orchestrator.dart
test/services/free_ram_gate_test.dart
doc/PLAN-features.md
Enable deterministic open-app handling via a natural-language parser, device tool capability suffix, and tool wiring.
  • Add OpenAppIntentParser utility to map English/German "open app" queries to known Android package names using normalized matching and intent detection.
  • In ModelOrchestrator, detect open-app intents before normal inference, run ToolExecutorService.openApp, and yield an InferenceResult describing success/failure including a synthetic toolCall payload.
  • Extend ModelOrchestrator._systemInstruction to accept optional text tools and append an Android-specific tool capability suffix listing available tools and JSON fallback instructions when appropriate.
  • Update NovaTools.open_app description and parameter documentation to include English/German verbs and common package examples.
  • Add unit tests for OpenAppIntentParser covering English/German, "aufmachen" forms, and negative cases (e.g., avoiding "geöffnet").
lib/utils/open_app_intent_parser.dart
lib/services/model_orchestrator.dart
lib/tools/tool_definitions.dart
test/utils/open_app_intent_parser_test.dart
Refine voice dictation UX by buffering full transcripts, exposing partial results, and avoiding auto-send when generation is in progress.
  • Enhance VoiceInputButton to accept an optional onPartial callback, manage a _flushing flag, and centralize stop/flush behavior in _finishListening, triggered both by button press and speech engine status callbacks.
  • Change speech listen options to longer listenFor/pauseFor durations, accumulate recognizedWords without auto-sending on finalResult, and invoke onPartial with live text.
  • Wire onPartial and updated onTranscription handling into AssistantScreen and AssistantScreenBeginner so the text field shows live dictation, and avoid auto-sending when a response is already generating or the orchestrator is busy.
lib/widgets/voice_input.dart
lib/screens/assistant_screen.dart
lib/screens/assistant_screen_beginner.dart
Switch chat export from filesystem writes to in-memory text/JSON, and route export through a shared ExportService-based sharing flow.
  • Refactor ChatHistoryService exportAsText/exportConversationAsText/exportAsJson to build and return in-memory strings only, extracting common message formatting into a private _appendMessage helper.
  • Update AssistantScreen export menu to call the new export APIs, handle empty content gracefully, and share via ExportService.instance.shareText with user feedback snackbars.
  • Adjust ChatHistoryScreen conversation export to use ExportService, show appropriate success/failure snackbars, and remove filesystem path handling.
  • Add a widget test ensuring exportAsText returns content without file paths or .txt suffixes.
lib/services/chat_history_service.dart
lib/screens/assistant_screen.dart
lib/screens/chat_history_screen.dart
test/services/chat_history_export_test.dart
Improve natural-language alarm parsing to support German phrases and add tests.
  • Extend AlarmTimeParser intent and time regexes to cover German alarm phrases (e.g., "stell einen Wecker", "Wecker auf 7 Uhr") and "Uhr"-style time expressions.
  • Update tryParse to recognize and parse German "Uhr" formats into 24-hour hour/minute pairs.
  • Add tests validating German alarm phrases and times are parsed correctly while preserving existing behavior.
lib/utils/alarm_time_parser.dart
test/utils/alarm_time_parser_test.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Block cold loads of Gemma 4 / mid-size models on Android when ActivityManager reports insufficient free RAM, with a clear fallback suggestion to smaller models.

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

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces several key enhancements, including a free-RAM hard gate before loading heavy models on Android, a deterministic 'open app' shortcut with German intent parsing, German language support for alarm parsing, direct conversation export via a share sheet (avoiding local file writes), and improved voice input handling with live previews. The review feedback highlights opportunities to improve robustness and efficiency, such as returning null instead of stale cached memory values on failure, making the German 'mach auf' regex more flexible, wrapping speech stop calls in try-catch blocks to prevent stuck UI states, updating text controllers in a single transaction to avoid cursor glitches, and optimizing the app intent parser's alias matching logic.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

}
}

return _lastAvailMemMb;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If the platform channel call fails or throws a PlatformException, returning _lastAvailMemMb will return a stale cached value from a previous successful reading. Since system RAM changes dynamically, using a stale value could lead to false positives (incorrectly blocking the user) or false negatives (crashing due to LMK). It is safer to return null on failure to ensure the gate is soft-skipped as intended.

Suggested change
return _lastAvailMemMb;
return null;

Comment on lines +11 to +14
static final RegExp _machAufIntent = RegExp(
r'\bmach(?:en)?\s+\S+\s+auf\b',
caseSensitive: false,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The _machAufIntent regex uses \S+ which matches exactly one non-whitespace word. In German, it is extremely common to have multiple words or articles between "mach" and "auf" (e.g., "mach die Kamera auf" or "mach bitte YouTube auf"). Using [^.!?]+? instead of \S+ makes the intent parser much more robust while still preventing matches from spanning across multiple sentences.

Suggested change
static final RegExp _machAufIntent = RegExp(
r'\bmach(?:en)?\s+\S+\s+auf\b',
caseSensitive: false,
);
static final RegExp _machAufIntent = RegExp(
r'\bmach(?:en)?\s+[^.!?]+?\s+auf\b',
caseSensitive: false,
);

Comment on lines +101 to 115
Future<void> _finishListening() async {
if (_flushing) return;
_flushing = true;
try {
await _speech.stop();
if (mounted) setState(() => _isListening = false);
if (_lastWords.isNotEmpty) {
widget.onTranscription(_lastWords);
_lastWords = '';
final transcript = _lastWords.trim();
_lastWords = '';
if (transcript.isNotEmpty) {
widget.onTranscription(transcript);
}
} else {
await _startListening();
} finally {
_flushing = false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If _speech.stop() throws an exception (e.g., if the speech engine is in an unexpected state), the setState(() => _isListening = false) call will be skipped. This will leave the UI permanently stuck in the "listening" state. Wrapping _speech.stop() in a try-catch block ensures that the UI state is always cleaned up and reset gracefully.

  Future<void> _finishListening() async {
    if (_flushing) return;
    _flushing = true;
    try {
      try {
        await _speech.stop();
      } catch (e) {
        debugPrint('Error stopping speech recognition: $e');
      }
      if (mounted) setState(() => _isListening = false);
      final transcript = _lastWords.trim();
      _lastWords = '';
      if (transcript.isNotEmpty) {
        widget.onTranscription(transcript);
      }
    } finally {
      _flushing = false;
    }
  }

Comment on lines +2125 to +2130
onPartial: (partial) {
_inputController.text = partial;
_inputController.selection = TextSelection.collapsed(
offset: partial.length,
);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Updating the text and selection separately triggers the controller's listeners twice, which can cause cursor jump glitches or redundant UI rebuilds. It is more idiomatic in Flutter to update both text and selection in a single transaction by setting the value property of the TextEditingController.

                    onPartial: (partial) {
                      _inputController.value = TextEditingValue(
                        text: partial,
                        selection: TextSelection.collapsed(
                          offset: partial.length,
                        ),
                      );
                    },

Comment on lines +492 to +497
onPartial: (partial) {
_inputController.text = partial;
_inputController.selection = TextSelection.collapsed(
offset: partial.length,
);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Updating the text and selection separately triggers the controller's listeners twice, which can cause cursor jump glitches or redundant UI rebuilds. It is more idiomatic in Flutter to update both text and selection in a single transaction by setting the value property of the TextEditingController.

            onPartial: (partial) {
              _inputController.value = TextEditingValue(
                text: partial,
                selection: TextSelection.collapsed(
                  offset: partial.length,
                ),
              );
            },

Comment on lines +57 to +71
static String? tryParsePackage(String query) {
if (!_hasOpenIntent(query)) return null;

final lower = query.toLowerCase();
// Prefer longer aliases first (e.g. "google maps" before "maps").
final aliases = knownApps.keys.toList()
..sort((a, b) => b.length.compareTo(a.length));
for (final alias in aliases) {
if (lower.contains(alias)) {
return knownApps[alias];
}
}

return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Sorting the aliases list on every call to tryParsePackage is inefficient. Additionally, simply checking lower.contains(alias) in descending order of length can return the wrong app if multiple apps are mentioned or if there are substring overlaps. A more robust and efficient approach is to find all matching aliases in a single pass, picking the one that appears earliest in the query, and using length as a tie-breaker.

  static String? tryParsePackage(String query) {
    if (!_hasOpenIntent(query)) return null;

    final lower = query.toLowerCase();
    String? bestAlias;
    int bestIndex = -1;

    for (final alias in knownApps.keys) {
      final index = lower.indexOf(alias);
      if (index != -1) {
        if (bestIndex == -1 || index < bestIndex) {
          bestIndex = index;
          bestAlias = alias;
        } else if (index == bestIndex) {
          if (bestAlias == null || alias.length > bestAlias.length) {
            bestAlias = alias;
          }
        }
      }
    }

    return bestAlias != null ? knownApps[bestAlias] : null;
  }

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lib/services/memory_diagnostics_service.dart" line_range="26-35" />
<code_context>
+  int? get lastAvailMemMb => _lastAvailMemMb;
+
+  /// Best-effort free system RAM in MB (Android ActivityManager).
+  Future<int?> readAvailableMemMb() async {
+    if (!kIsWeb && Platform.isAndroid) {
+      try {
+        final result = await _channel.invokeMethod<Map<Object?, Object?>>(
+          'getProcessMemory',
+        );
+        if (result != null) {
+          _lastPssKb = result['pssKb'] as int?;
+          _lastRssKb = result['rssKb'] as int?;
+          final avail = result['availMemMb'];
+          if (avail is int) {
+            _lastAvailMemMb = avail;
+
+            return avail;
+          }
+          if (avail is num) {
+            _lastAvailMemMb = avail.round();
+
+            return _lastAvailMemMb;
+          }
+        }
</code_context>
<issue_to_address>
**suggestion:** Factor out shared parsing of `availMemMb` to reduce duplication and keep fields in sync.

Both methods currently parse `availMemMb` and update `_lastAvailMemMb` slightly differently. A small private helper that takes the result map and sets `_lastPssKb`, `_lastRssKb`, and `_lastAvailMemMb` in one place would remove duplication and guarantee these fields stay consistent across both code paths.

Suggested implementation:

```
  int? _lastRssKb;
  int? _lastAvailMemMb;

  int? get lastPssMb =>
      _lastPssKb != null ? (_lastPssKb! / 1024).round() : null;
  int? get lastRssMb =>
      _lastRssKb != null ? (_lastRssKb! / 1024).round() : null;
  int? get lastAvailMemMb => _lastAvailMemMb;

  /// Parses process memory info from a platform channel result map and updates
  /// [_lastPssKb], [_lastRssKb] and [_lastAvailMemMb] consistently.
  ///
  /// Returns the parsed available memory in MB, or null if it is not present.
  int? _updateProcessMemoryFromResult(Map<Object?, Object?> result) {
    _lastPssKb = result['pssKb'] as int?;
    _lastRssKb = result['rssKb'] as int?;

    final avail = result['availMemMb'];
    if (avail is int) {
      _lastAvailMemMb = avail;
    } else if (avail is num) {
      _lastAvailMemMb = avail.round();
    } else {
      _lastAvailMemMb = null;
    }

    return _lastAvailMemMb;
  }

```

```
        if (result != null) {
          return _updateProcessMemoryFromResult(result);
        }

```

There is at least one other method in this service that also parses `pssKb`, `rssKb`, and `availMemMb` from the platform channel result. To fully apply the refactor and keep these fields in sync, update that method to call `_updateProcessMemoryFromResult(result)` instead of duplicating the parsing logic.
</issue_to_address>

### Comment 2
<location path="lib/widgets/voice_input.dart" line_range="101-115" />
<code_context>
+    }
+  }
+
+  Future<void> _finishListening() async {
+    if (_flushing) return;
+    _flushing = true;
+    try {
       await _speech.stop();
       if (mounted) setState(() => _isListening = false);
-      if (_lastWords.isNotEmpty) {
-        widget.onTranscription(_lastWords);
-        _lastWords = '';
+      final transcript = _lastWords.trim();
+      _lastWords = '';
+      if (transcript.isNotEmpty) {
+        widget.onTranscription(transcript);
       }
-    } else {
-      await _startListening();
+    } finally {
+      _flushing = false;
     }
   }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Guard `_finishListening` against errors from `_speech.stop()` to keep state consistent.

If `_speech.stop()` throws (e.g., from a plugin error), `_flushing` will reset but `_isListening` will stay true and `_lastWords` may be out of sync. Consider wrapping only `_speech.stop()` in a try/catch, logging the error, and explicitly forcing `_isListening = false` (and any other cleanup) in the catch block so the UI can recover from a failed stop.

```suggestion
  Future<void> _finishListening() async {
    if (_flushing) return;
    _flushing = true;
    try {
      try {
        await _speech.stop();
      } catch (error, stackTrace) {
        debugPrint('Error stopping speech recognition: $error');
        debugPrint('$stackTrace');
        if (mounted) {
          setState(() {
            _isListening = false;
          });
        } else {
          _isListening = false;
        }
        _lastWords = '';
        return;
      }

      if (mounted) setState(() => _isListening = false);
      final transcript = _lastWords.trim();
      _lastWords = '';
      if (transcript.isNotEmpty) {
        widget.onTranscription(transcript);
      }
    } finally {
      _flushing = false;
    }
  }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +26 to +35
Future<int?> readAvailableMemMb() async {
if (!kIsWeb && Platform.isAndroid) {
try {
final result = await _channel.invokeMethod<Map<Object?, Object?>>(
'getProcessMemory',
);
if (result != null) {
_lastPssKb = result['pssKb'] as int?;
_lastRssKb = result['rssKb'] as int?;
final avail = result['availMemMb'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Factor out shared parsing of availMemMb to reduce duplication and keep fields in sync.

Both methods currently parse availMemMb and update _lastAvailMemMb slightly differently. A small private helper that takes the result map and sets _lastPssKb, _lastRssKb, and _lastAvailMemMb in one place would remove duplication and guarantee these fields stay consistent across both code paths.

Suggested implementation:

  int? _lastRssKb;
  int? _lastAvailMemMb;

  int? get lastPssMb =>
      _lastPssKb != null ? (_lastPssKb! / 1024).round() : null;
  int? get lastRssMb =>
      _lastRssKb != null ? (_lastRssKb! / 1024).round() : null;
  int? get lastAvailMemMb => _lastAvailMemMb;

  /// Parses process memory info from a platform channel result map and updates
  /// [_lastPssKb], [_lastRssKb] and [_lastAvailMemMb] consistently.
  ///
  /// Returns the parsed available memory in MB, or null if it is not present.
  int? _updateProcessMemoryFromResult(Map<Object?, Object?> result) {
    _lastPssKb = result['pssKb'] as int?;
    _lastRssKb = result['rssKb'] as int?;

    final avail = result['availMemMb'];
    if (avail is int) {
      _lastAvailMemMb = avail;
    } else if (avail is num) {
      _lastAvailMemMb = avail.round();
    } else {
      _lastAvailMemMb = null;
    }

    return _lastAvailMemMb;
  }

        if (result != null) {
          return _updateProcessMemoryFromResult(result);
        }

There is at least one other method in this service that also parses pssKb, rssKb, and availMemMb from the platform channel result. To fully apply the refactor and keep these fields in sync, update that method to call _updateProcessMemoryFromResult(result) instead of duplicating the parsing logic.

Comment on lines +101 to 115
Future<void> _finishListening() async {
if (_flushing) return;
_flushing = true;
try {
await _speech.stop();
if (mounted) setState(() => _isListening = false);
if (_lastWords.isNotEmpty) {
widget.onTranscription(_lastWords);
_lastWords = '';
final transcript = _lastWords.trim();
_lastWords = '';
if (transcript.isNotEmpty) {
widget.onTranscription(transcript);
}
} else {
await _startListening();
} finally {
_flushing = false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Guard _finishListening against errors from _speech.stop() to keep state consistent.

If _speech.stop() throws (e.g., from a plugin error), _flushing will reset but _isListening will stay true and _lastWords may be out of sync. Consider wrapping only _speech.stop() in a try/catch, logging the error, and explicitly forcing _isListening = false (and any other cleanup) in the catch block so the UI can recover from a failed stop.

Suggested change
Future<void> _finishListening() async {
if (_flushing) return;
_flushing = true;
try {
await _speech.stop();
if (mounted) setState(() => _isListening = false);
if (_lastWords.isNotEmpty) {
widget.onTranscription(_lastWords);
_lastWords = '';
final transcript = _lastWords.trim();
_lastWords = '';
if (transcript.isNotEmpty) {
widget.onTranscription(transcript);
}
} else {
await _startListening();
} finally {
_flushing = false;
}
}
Future<void> _finishListening() async {
if (_flushing) return;
_flushing = true;
try {
try {
await _speech.stop();
} catch (error, stackTrace) {
debugPrint('Error stopping speech recognition: $error');
debugPrint('$stackTrace');
if (mounted) {
setState(() {
_isListening = false;
});
} else {
_isListening = false;
}
_lastWords = '';
return;
}
if (mounted) setState(() => _isListening = false);
final transcript = _lastWords.trim();
_lastWords = '';
if (transcript.isNotEmpty) {
widget.onTranscription(transcript);
}
} finally {
_flushing = false;
}
}

@involvex
involvex merged commit b8a2d3b into main Jul 18, 2026
6 checks passed
'When the user asks to open an app, set/cancel an alarm, open settings, '
'or search the web, call the tool immediately — do not say you lack '
'device access. '
'open_app packages: com.google.android.youtube, com.android.settings, '

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: _deviceToolsCapabilitySuffix hardcodes only 3 packages while OpenAppIntentParser.knownApps supports 9 unique packages. When native function calling is unavailable and the model falls back to text-based JSON, the model cannot construct valid tool calls for packages not listed here (e.g., WhatsApp, Spotify, Maps).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
WARNING 1
Issue Details (click to expand)

WARNING

File Line Issue
lib/services/model_orchestrator.dart 2409 _deviceToolsCapabilitySuffix hardcodes only 3 packages while OpenAppIntentParser.knownApps supports 9 unique packages. When native function calling is unavailable and the model falls back to text-based JSON, the model cannot construct valid tool calls for packages not listed here (e.g., WhatsApp, Spotify, Maps).
Files Reviewed (16 files)
  • doc/PLAN-features.md
  • lib/screens/assistant_screen.dart
  • lib/screens/assistant_screen_beginner.dart
  • lib/screens/chat_history_screen.dart
  • lib/services/chat_history_service.dart
  • lib/services/memory_diagnostics_service.dart
  • lib/services/model_orchestrator.dart - 1 issue
  • lib/services/platform_adaptation_service.dart
  • lib/tools/tool_definitions.dart
  • lib/utils/alarm_time_parser.dart
  • lib/utils/open_app_intent_parser.dart
  • lib/widgets/voice_input.dart
  • test/services/chat_history_export_test.dart
  • test/services/free_ram_gate_test.dart
  • test/utils/alarm_time_parser_test.dart
  • test/utils/open_app_intent_parser_test.dart

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 141.7K · Output: 35.1K · Cached: 1.9M

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant