Skip to content

fix: recover SmolLM/Gemma stability + speed up model downloads#7

Merged
involvex merged 2 commits into
mainfrom
fix/download-throughput
Jul 19, 2026
Merged

fix: recover SmolLM/Gemma stability + speed up model downloads#7
involvex merged 2 commits into
mainfrom
fix/download-throughput

Conversation

@involvex

@involvex involvex commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Cherry-picks the unpushed SmolLM/Gemma/UI session fix that missed PR fix: SharedPreferences OOM + low-RAM onboarding for mid-range devices #6 (never pushed before merge).
  • Throttles in-app download progress to percent-change only so large HF pulls (Gemma 4 ~2.5GB) are not starved by per-chunk setState/GC.
  • Raises orchestrator network-install timeout from 5 minutes to 45 minutes for Gemma-scale models.

Test plan

  • Settings → download FastVLM or Gemma 3/4 with HF token; progress updates ~1%/step, throughput multi-MB/s on WiFi
  • SmolLM short chat: no ChatML regurgitation / OUT_OF_RANGE
  • Import custom Gemma .litertlm: loads that model (not SmolLM)
  • No TextEditingController dispose crash after import/edit

Made with Cursor

Summary by Sourcery

Stabilize SmolLM and Gemma sessions and improve reliability of large model installs and chats.

Bug Fixes:

  • Reset and reinject chat sessions safely after edit/regenerate to avoid stale or broken inference sessions and empty responses.
  • Handle inference stream failures by dropping dead sessions and surfacing clear, user-facing error guidance.
  • Prevent crashes from disposing text controllers after dialogs by deferring disposal to a post-frame callback.
  • Avoid empty markdown rendering issues by falling back to simple text when responses are blank.

Enhancements:

  • Introduce robust model load orchestration with epoch-based timeouts and teardown to avoid reusing partial or stale GPU sessions.
  • Adjust model selection, tool prompting, and backend choice for SmolLM and Gemma (including vision routing) to favor stable, appropriate models per device and query.
  • Throttle download progress updates and extend network install timeouts so large HuggingFace model downloads complete reliably without UI thrash.
  • Require and surface HuggingFace token prerequisites for gated Gemma downloads with clear blocking messages and actions.
  • Refine custom model import and selection flows, including backend heuristics and sheet return values, to better integrate imported models into the assistant.
  • Add targeted agent debug logging around model loading, replay, and streaming paths to support field diagnostics.

Documentation:

  • Update planning docs with links and notes for upcoming features and device constraints.

Tests:

  • Add coverage to ensure SmolLM does not advertise function calling and has an increased KV token limit.

involvex and others added 2 commits July 19, 2026 01:49
Prevent KV overflow and ChatML regurgitation on SmolLM, honor preferred/custom model selection, gate gated HF downloads, and stop modal/edit dispose crashes after import.

Co-authored-by: Cursor <cursoragent@cursor.com>
Per-chunk UI progress callbacks starved the event loop on large HF downloads. Report percent only when it changes and allow 45 minutes for Gemma-scale installs.

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

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

Refactors model loading, teardown, and session management to make SmolLM/Gemma inference more stable, gate Gemma downloads behind HF tokens, throttle large model download progress updates, and improve UX flows around custom model import, screenshot questions, and error/empty responses.

Sequence diagram for updated model load with timeout and stale completion handling

sequenceDiagram
  participant ModelOrchestrator
  participant MemoryDiagnosticsService
  participant PlatformAdaptationService
  participant FlutterGemma
  participant InferenceModel

  ModelOrchestrator->>MemoryDiagnosticsService: readTotalMemMb()
  MemoryDiagnosticsService-->>ModelOrchestrator: lastTotalMemMb
  ModelOrchestrator->>PlatformAdaptationService: preferredBackendFor(modelToLoad)
  PlatformAdaptationService-->>ModelOrchestrator: PreferredBackend

  ModelOrchestrator->>ModelOrchestrator: _loadActiveModelWithTimeout(modelToLoad, preferredBackend, supportImage)
  activate ModelOrchestrator
  ModelOrchestrator->>ModelOrchestrator: _loadEpoch++ (capture epoch)
  ModelOrchestrator->>FlutterGemma: getActiveModel(maxTokens, preferredBackend, supportImage, maxNumImages)
  activate FlutterGemma

  alt load completes before timeout
    FlutterGemma-->>ModelOrchestrator: InferenceModel
    deactivate FlutterGemma
    ModelOrchestrator->>ModelOrchestrator: check epoch == _loadEpoch
    alt epoch unchanged
      ModelOrchestrator-->>ModelOrchestrator: return InferenceModel
    else epoch changed (stale)
      ModelOrchestrator->>InferenceModel: close()
      InferenceModel-->>ModelOrchestrator: done
      ModelOrchestrator->>ModelOrchestrator: _clearActiveInferenceIdentity()
      ModelOrchestrator-->>ModelOrchestrator: throw TimeoutException
    end
  else TimeoutException
    ModelOrchestrator->>ModelOrchestrator: _loadEpoch++
    ModelOrchestrator-->>ModelOrchestrator: rethrow TimeoutException
    ModelOrchestrator->>FlutterGemma: getActiveModel(...) future.then(...)
    activate FlutterGemma
    FlutterGemma-->>ModelOrchestrator: late InferenceModel
    deactivate FlutterGemma
    ModelOrchestrator->>InferenceModel: close()
    InferenceModel-->>ModelOrchestrator: done
    ModelOrchestrator->>ModelOrchestrator: _clearActiveInferenceIdentity()
  end
  deactivate ModelOrchestrator

  Note over ModelOrchestrator,FlutterGemma: _teardownPartialLoad() also bumps _loadEpoch and closes _activeModel on partial failures
Loading

File-Level Changes

Change Details Files
Harden model load/teardown lifecycle and prevent reuse of broken SmolLM/Gemma sessions.
  • Introduce a load epoch counter and a _loadActiveModelWithTimeout helper that discards late FlutterGemma.getActiveModel completions and eagerly closes stale models.
  • Make _teardownActiveModel async, clear inference identity even when no active model exists, and reuse it across partial-load and custom-model paths.
  • Adjust per-model load timeouts (longer on Android and for Gemma 4) and route all main catalog/custom loads through the new timeout helper.
  • On stream errors or empty replies, drop the active chat session, surface user-facing hints, and avoid persisting empty responses to memory storage.
lib/services/model_orchestrator.dart
lib/utils/message_limits.dart
test/utils/message_limits_test.dart
Fix chat replay after edit/regenerate/retry and make SmolLM prompt/tool behavior safer.
  • Add invalidateSessionForReplay to clear _activeChat and stage pending messages for reinjection after UI edits or retries, and call it from assistant/beginner screens wherever history is truncated.
  • Refine history reinjection to account for system prompt token overhead, recreate the chat session if clearHistory fails, and emit a user-visible warning when history cannot be restored.
  • Ensure explicit model overrides (e.g., Gemma 3 selection) are honored across turns, add auto vision-model selection based on installed assets, and gate tool sermons/function-calling prompts away from SmolLM.
  • Update NovaModel.supportsFunctionCalling and system-prompt generation to treat SmolLM as non-FC with compact prompts and no tool sermon.
lib/services/model_orchestrator.dart
lib/screens/assistant_screen.dart
lib/screens/assistant_screen_beginner.dart
lib/models/model_info.dart
test/models/model_info_test.dart
Improve model download robustness, progress reporting, and Hugging Face token handling for Gemma models.
  • Throttle network download progress callbacks to integer-percent steps, remove asBroadcastStream, and ensure progress reaches 90% after transfer completes so large downloads do not stall the UI.
  • Extend orchestrator network install timeout from 5 minutes to 45 minutes to accommodate Gemma 4-sized models.
  • Detect Gemma/gated HF URLs and block downloads without a configured Hugging Face token, logging via AgentDebugLog and surfacing a settings/token SnackBar in the settings screen.
lib/services/model_manager.dart
lib/models/model_info.dart
lib/services/model_orchestrator.dart
lib/screens/settings_screen.dart
Polish assistant UI flows for screenshots, voice, and custom model import to avoid crashes and improve UX.
  • Factor out _wantsDeviceScreen and require an actual screenshot/image before answering "what's on my screen" questions, auto-capturing when possible and warning the user if capture fails.
  • Guard setState and controller updates with mounted checks, dispose TextEditingControllers via post-frame callbacks to avoid dispose-after-use crashes, and use unawaited(_sendMessage()) to avoid blocking UI event handlers.
  • Rework model selector and custom model import sheets so imports return the selected CustomModel via Navigator.pop, then set preferred model/custom model on the ModelOrchestrator and show consistent SnackBars.
lib/screens/assistant_screen.dart
lib/screens/model_browser_screen.dart
lib/screens/custom_model_import_sheet.dart
lib/screens/settings_screen.dart
Handle empty/error chat bubbles gracefully and add observability around model behavior for soak testing.
  • Avoid rendering empty Markdown bodies in ChatBubble by showing a simple placeholder label when text is empty and not streaming, preventing layout asserts on failed turns.
  • Add AgentDebugLog calls around session invalidation, backend selection, model resolution, chat prep/reinjection, download gating, and stream errors to aid debugging on real devices.
  • Update docs to reference new planning documents and clarify hygiene tasks for removing agent-debug scaffolding later.
lib/widgets/chat_bubble.dart
lib/services/model_orchestrator.dart
lib/screens/assistant_screen.dart
lib/services/model_manager.dart
doc/PLAN-features.md

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

@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 left some high level feedback:

  • Several AgentDebugLog.log calls are awaited directly in hot paths like processMessage and model loading; consider making these fire-and-forget (with appropriate error swallowing) to avoid adding avoidable latency or blocking user-visible flows on logging I/O.
  • The HuggingFace gating logic is split between requiresHuggingFaceAuth(NovaModel) and urlRequiresHuggingFaceAuth(String) and used in different places; consolidating these into a single, model-centric check would reduce duplication and the risk of the two paths diverging over time.
  • The _loadEpoch mechanism is updated in multiple code paths (_loadActiveModelWithTimeout, _teardownPartialLoad, timeout handlers); it may be worth adding a brief comment or small helper around epoch increments to make the intended sequencing/race guarantees clearer for future maintainers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several `AgentDebugLog.log` calls are `await`ed directly in hot paths like `processMessage` and model loading; consider making these fire-and-forget (with appropriate error swallowing) to avoid adding avoidable latency or blocking user-visible flows on logging I/O.
- The HuggingFace gating logic is split between `requiresHuggingFaceAuth(NovaModel)` and `urlRequiresHuggingFaceAuth(String)` and used in different places; consolidating these into a single, model-centric check would reduce duplication and the risk of the two paths diverging over time.
- The `_loadEpoch` mechanism is updated in multiple code paths (`_loadActiveModelWithTimeout`, `_teardownPartialLoad`, timeout handlers); it may be worth adding a brief comment or small helper around epoch increments to make the intended sequencing/race guarantees clearer for future maintainers.

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.

@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 enhancements and stability fixes for model management and inference orchestration, including session invalidation for replay on message edits, HuggingFace token checks for gated Gemma models, optimized download progress reporting, and forcing the CPU backend for SmolLM on low-RAM Android devices. Feedback on these changes highlights critical asynchronous issues, such as a race condition in _teardownActiveModel where the active model is not cleared synchronously before closing, a potential RangeError when mutating the message list after an asynchronous gap, a missing mounted check before calling setState in the settings screen, and a bug where late timeout cleanups could inadvertently clear the active identity of a newly loaded model.

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.

Comment on lines +352 to 371
Future<void> _teardownActiveModel({NovaModel? keepIfSameType}) async {
_activeChat = null;
if (_activeModel == null) return;
if (_activeModel == null) {
await _clearActiveInferenceIdentity();

return;
}
final shouldClose =
keepIfSameType == null ||
_activeModelType == null ||
keepIfSameType != _activeModelType;
if (!shouldClose) return;
_activeModel!.close().catchError((_) {});
try {
await _activeModel!.close();
} catch (_) {}
_activeModel = null;
_activeModelType = null;
_activeModelSupportsImage = false;
_clearActiveInferenceIdentity();
await _clearActiveInferenceIdentity();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

There is a critical race condition in _teardownActiveModel. Because this method is asynchronous and unawaited in multiple places (e.g., lines 330, 349, 756), _activeModel remains non-null while await _activeModel!.close() is executing. If another operation (such as _getOrCreateModel or another teardown) runs concurrently, it will see _activeModel as non-null and potentially attempt to call close() on it again. Calling close() concurrently on the same native model instance can cause severe native crashes (SIGABRT/double-free).

To fix this, clear _activeModel, _activeModelType, and _activeModelSupportsImage synchronously before awaiting the asynchronous close() call.

  Future<void> _teardownActiveModel({NovaModel? keepIfSameType}) async {
    _activeChat = null;
    if (_activeModel == null) {
      await _clearActiveInferenceIdentity();

      return;
    }
    final shouldClose =
        keepIfSameType == null ||
        _activeModelType == null ||
        keepIfSameType != _activeModelType;
    if (!shouldClose) return;

    final modelToClose = _activeModel;
    _activeModel = null;
    _activeModelType = null;
    _activeModelSupportsImage = false;

    try {
      await modelToClose!.close();
    } catch (_) {}
    await _clearActiveInferenceIdentity();
  }

Comment on lines 713 to 717
await TtsService.instance.stop();
if (!mounted) return;
setState(() {
_messages.removeRange(userIndex, _messages.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.

high

There is an asynchronous gap (await TtsService.instance.stop()) before _messages.removeRange is called. During this await, the user could have cleared the chat history or switched sessions, meaning _messages could be empty or smaller than userIndex. Calling removeRange without validating userIndex against the current _messages.length will throw a RangeError and crash the UI.

Validate that userIndex is still within bounds of _messages after the asynchronous gap.

Suggested change
await TtsService.instance.stop();
if (!mounted) return;
setState(() {
_messages.removeRange(userIndex, _messages.length);
});
await TtsService.instance.stop();
if (!mounted) return;
if (userIndex < 0 || userIndex >= _messages.length) return;
setState(() {
_messages.removeRange(userIndex, _messages.length);
});

Comment on lines +1269 to 1289
if (ModelHuggingFaceURLs.requiresHuggingFaceAuth(model) &&
!await ModelManager.hasHuggingFaceToken()) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'Gemma models need a HuggingFace token. Add one in Settings.',
),
backgroundColor: Colors.orange[800],
action: SnackBarAction(
label: 'Add token',
textColor: Colors.white,
onPressed: () => _showHfTokenDialog(context),
),
),
);

return;
}

setState(() => _installStatus = 'Downloading ${model.displayName}...');

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

In _downloadModel, ModelManager.hasHuggingFaceToken() is awaited. If the token check succeeds, the code proceeds to call setState at line 1289 without checking if the widget is still mounted. If the user exits the settings screen while the token check is pending, this will trigger a "setState() called after dispose()" crash.

Ensure mounted is checked before calling setState after the asynchronous gap.

    if (ModelHuggingFaceURLs.requiresHuggingFaceAuth(model) &&
        !await ModelManager.hasHuggingFaceToken()) {
      if (!context.mounted) return;
      ScaffoldMessenger.of(context).showSnackBar( 
        SnackBar(
          content: const Text(
            'Gemma models need a HuggingFace token. Add one in Settings.',
          ),
          backgroundColor: Colors.orange[800],
          action: SnackBarAction(
            label: 'Add token',
            textColor: Colors.white,
            onPressed: () => _showHfTokenDialog(context),
          ),
        ),
      );

      return;
    }

    if (!mounted) return;
    setState(() => _installStatus = 'Downloading ${model.displayName}...');

Comment on lines +662 to +675
} on TimeoutException {
_loadEpoch++;
unawaited(
future
.then((lateModel) async {
try {
await lateModel.close();
} catch (_) {}
await _clearActiveInferenceIdentity();
})
.catchError((_) {}),
);
rethrow;
}

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

In _loadActiveModelWithTimeout, if a model load times out, a late completion callback is registered to close the late model and call _clearActiveInferenceIdentity(). However, if a new model has already successfully loaded in the meantime, calling _clearActiveInferenceIdentity() will clear the active identity of the newly loaded model, breaking its session.

Only call _clearActiveInferenceIdentity() in the late completion callback if no other model is currently active (i.e., _activeModel == null).

    } on TimeoutException {
      _loadEpoch++;
      unawaited(
        future
            .then((lateModel) async {
              try {
                await lateModel.close();
              } catch (_) {}
              if (_activeModel == null) {
                await _clearActiveInferenceIdentity();
              }
            })
            .catchError((_) {}),
      );
      rethrow;
    }

@kilo-code-bot

kilo-code-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
HIGH 2
MEDIUM 1
Issue Details (click to expand)

CRITICAL

File Line Issue
lib/services/model_orchestrator.dart 371 _teardownActiveModel async race: _activeModel remains non-null while await close() executes, allowing concurrent close() calls on the same native instance

HIGH

File Line Issue
lib/screens/assistant_screen.dart 717 Missing bounds check after await TtsService.instance.stop(): userIndex can be out of range if messages were modified during the async gap
lib/screens/settings_screen.dart 1289 Missing mounted check after await ModelManager.hasHuggingFaceToken() before calling setState

MEDIUM

File Line Issue
lib/services/model_orchestrator.dart 675 Late completion callback in _loadActiveModelWithTimeout unconditionally calls _clearActiveInferenceIdentity(), which can clear the identity of a newer model loaded after the timeout
Files Reviewed (15 files)
  • doc/PLAN-features.md
  • lib/models/model_info.dart
  • lib/screens/assistant_screen.dart - 2 issues
  • lib/screens/assistant_screen_beginner.dart
  • lib/screens/custom_model_import_sheet.dart
  • lib/screens/model_browser_screen.dart
  • lib/screens/settings_screen.dart - 1 issue
  • lib/services/model_manager.dart
  • lib/services/model_orchestrator.dart - 2 issues
  • lib/services/platform_adaptation_service.dart
  • lib/utils/message_limits.dart
  • lib/widgets/chat_bubble.dart
  • test/models/model_info_test.dart
  • test/utils/message_limits_test.dart

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 294.5K · Output: 59.3K · Cached: 7.9M

@involvex
involvex merged commit c0429ac into main Jul 19, 2026
6 checks passed
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