Skip to content

release: onboarding reliability and post-0.0.103 fixes#571

Open
alichherawalla wants to merge 300 commits into
mainfrom
fix/onboarding-analyzing-device-hang
Open

release: onboarding reliability and post-0.0.103 fixes#571
alichherawalla wants to merge 300 commits into
mainfrom
fix/onboarding-analyzing-device-hang

Conversation

@alichherawalla

@alichherawalla alichherawalla commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Release candidate consolidating the onboarding reliability fix and all post-0.0.103 work onto main.

Scope note: despite the "onboarding" title, this branch carries ~300 commits and touches 159 source files across nearly every subsystem — chat/generation, text & image models, downloads, voice/TTS, RAG/KB, memory & residency, remote servers, Pro licensing, onboarding, and settings. Treat manual QA as a full-product pass, not an onboarding-only spot check.

Headline changes:

Type of Change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change
  • Chore (build/CI/deps)

Test coverage — automated vs. manual

Coverage is mapped per check in docs/RELEASE_TEST_CHECKLIST.csv (247 rows, each tagged with an Automation %). The targeted device pass is docs/PR_571_TARGETED_MANUAL_TESTS.md; the tonight-ship check-off sheet is docs/RELEASE_TONIGHT_CHECKOFF.md.

Covered by automated integration tests

327 real-screen / real-navigation / real-gesture integration suites (fakes only at the device boundary). Of 247 release checks: 72 fully automated, 107 at 70–99%, 55 partial, 13 at 0%. Strongest coverage: prompt enhancement (100%), image gen (90%), tools (90%), KB/projects (86%), remote (83%), text gen (80%).

Must be validated manually on device

  • Zero automated coverage (13): performance vs. production (cold/warm start, cold load + first token, warm decode, sustained/thermal soak, large-chat scroll, evidence record), slide-to-cancel pill + cancel-vs-send, SDXL Core ML finalize + first ANE compile, low-RAM curated LiteRT warning, TTS memory-pressure failure, iOS Debug build name.
  • P0 correctness only partly automated (highest priority): fresh install/boot & upgrade-over-install data retention (1, 181, 195, 187, 18, 171); first-send load + Stop mid-gen (23, 43); Gemma-4 native-first thinking+tool (180); voice/dictation & full STT→TTS (53, 54, 55, 60, 215); memory residency (86, 87, 88, 93, 99, 101); image render (66); security & Pro (228, 229, 231, 234); fault recovery (237–240).
  • Both platforms, both themes, recorded independently. iOS has no local signal (see below).

Checklist

Testing

  • Tested on Android (native compile, lint, unit suite)
  • Tested on iOS — not run locally (configured iPhone 16e simulator not installed); CI/device QA must provide this signal
  • Light mode and dark mode — pending device QA
  • Existing tests pass locally (npm test)
  • Added tests proving the fix/feature

Security

  • No secrets/keys/credentials in code
  • User input validated/sanitized where applicable

Related Issues

Supersedes #560, #561, #568, #569.

Additional Notes

Local verification:

  • 557 Jest suites passed; 7,819 tests passed, 4 skipped.
  • Coverage: 90.44% statements, 84.18% branches, 85.74% functions, 91.72% lines.
  • Targeted onboarding/model regressions: 7 suites, 70 tests passed.
  • TypeScript, ESLint, dependency-cruiser, and knip passed.
  • Android compile, lint, tests passed (BUILD SUCCESSFUL, 589 tasks).
  • iOS not run locally — simulator missing; requires CI/device QA.
  • Jest reports pre-existing post-teardown async leaks under force-exit; tracked as audit debt, not introduced here.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The pull request adds RAM-based model fit tiers and compact fit badges, introduces timeout-aware model-file loading with inline retry UI, makes onboarding device analysis non-blocking, updates total-RAM display, hardens download and release scripts, and modernizes numerous integration assertions and repository instructions.

Model fit and onboarding

Layer / File(s) Summary
Fit-tier model browsing and presentation
src/services/memoryBudget.ts, src/screens/ModelsScreen/*, src/components/ModelCardContent.tsx, src/types/index.ts, __tests__/integration/models/*
Models and files are classified as easy, fits, tight, or wontFit; loadable models remain visible and compact cards display the fit tier.
Model-file timeout and retry flow
src/services/huggingface.ts, src/screens/ModelsScreen/*, __tests__/integration/models/modelFilesLoadErrorShowsRetry.rendered.test.tsx
Timed-out file requests surface an inline error with a Retry action instead of being treated as an empty compatible-file result.
Non-blocking onboarding readiness
src/screens/ModelDownloadScreen.tsx, __tests__/integration/onboarding/*, __tests__/rntl/screens/ModelDownloadScreen.test.tsx
Onboarding renders after device recommendation data is ready, displays total physical RAM, and uses rendered integration coverage for hardware behavior.

Reliability and maintenance

Layer / File(s) Summary
Download and release reliability
android/app/src/main/java/.../download/*, src/services/activeDownloadPersistence.ts, src/services/modelPreloader.ts, src/screens/DownloadManagerScreen/useVoiceDownloadItems.ts, fastlane/Fastfile, scripts/uat.sh, __tests__/unit/services/modelManager.test.ts
Download IDs and persistence signatures are normalized, failed cleanup is logged, optional hooks are awaited directly, Play promotion status is explicit, and UAT checks use safer conditionals and artifact validation.
Integration assertion modernization
__tests__/integration/**/*
Array counts use toHaveLength, null checks use toBeNull, dynamic-require lint suppressions are removed, and enhancement cases use a table-driven test.

Repository guidance

Layer / File(s) Summary
Canonical engineering guidance
AGENTS.md, CLAUDE.md
Engineering, testing, repository, quality-gate, and branch workflow guidance is consolidated in AGENTS.md, which CLAUDE.md references.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is strong overall, but it omits the mandatory screenshots section and several required checklist subsections from the template. Add the required Screenshots section for UI changes and fill out the missing General, React Native Specific, and Performance & Models checklist sections.
✅ Passed checks (3 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise and accurately reflects the PR’s release-focused reliability and post-0.0.103 fixes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onboarding-analyzing-device-hang

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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/services/huggingface.ts (1)

30-88: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Timeout clears prematurely, leaving the body stream unprotected.

fetchWithTimeout clears the timer as soon as fetch resolves (which happens when the HTTP headers are received). Consequently, await response.json() is executed after the timeout has been cleared. If the network connection drops or the server stalls while streaming the JSON body, the app will hang indefinitely on response.json(), bypassing the intended 5-second limit entirely.

Refactor the timeout logic into a wrapper that keeps the AbortSignal active until the body is fully read.

🛠️ Proposed fix using a holistic timeout wrapper

Replace fetchWithTimeout with withTimeout, and update fetchJson and getModelFiles to wrap the entire parse operation:

-  private async fetchWithTimeout(url: string, timeoutMs = HF_REQUEST_TIMEOUT_MS): Promise<Response> {
+  private async withTimeout<T>(operation: (signal: AbortSignal) => Promise<T>, timeoutMs = HF_REQUEST_TIMEOUT_MS): Promise<T> {
     const controller = new AbortController();
     const timer = setTimeout(() => controller.abort(), timeoutMs);
     try {
-      return await fetch(url, { headers: { Accept: 'application/json' }, signal: controller.signal });
+      return await operation(controller.signal);
     } finally {
       clearTimeout(timer);
     }
   }
 
   private async fetchJson<T>(url: string): Promise<T> {
-    const response = await this.fetchWithTimeout(url);
-    if (!response.ok) throw new Error(`API error: ${response.status}`);
-    return response.json() as Promise<T>;
+    return this.withTimeout(async (signal) => {
+      const response = await fetch(url, { headers: { Accept: 'application/json' }, signal });
+      if (!response.ok) throw new Error(`API error: ${response.status}`);
+      return response.json();
+    });
   }
 
// ...
 
   async getModelFiles(modelId: string): Promise<ModelFile[]> {
     try {
-      const response = await this.fetchWithTimeout(`${this.apiUrl}/models/${modelId}/tree/main`);
-      if (!response.ok) return this.getModelFilesFromSiblings(modelId);
-      const files: Array<{ type: string; path: string; size?: number; lfs?: { size: number } }> = await response.json();
-      const allGguf = files.filter(f => f.type === 'file' && f.path.endsWith('.gguf'));
-      const mmProjFiles = allGguf.filter(f => this.isMMProjFile(f.path));
-      const modelFiles = allGguf.filter(f => !this.isMMProjFile(f.path));
-      return modelFiles
-        .map(file => ({
-          name: file.path,
-          size: file.lfs?.size || file.size || 0,
-          quantization: this.extractQuantization(file.path),
-          downloadUrl: this.getDownloadUrl(modelId, file.path),
-          mmProjFile: this.findMatchingMMProj(file.path, mmProjFiles, modelId),
-        }))
-        .sort((a, b) => a.size - b.size);
+      return await this.withTimeout(async (signal) => {
+        const response = await fetch(`${this.apiUrl}/models/${modelId}/tree/main`, { headers: { Accept: 'application/json' }, signal });
+        if (!response.ok) throw new Error('API Error'); // Triggers the catch block to run the sibling fallback
+        const files: Array<{ type: string; path: string; size?: number; lfs?: { size: number } }> = await response.json();
+        const allGguf = files.filter(f => f.type === 'file' && f.path.endsWith('.gguf'));
+        const mmProjFiles = allGguf.filter(f => this.isMMProjFile(f.path));
+        const modelFiles = allGguf.filter(f => !this.isMMProjFile(f.path));
+        return modelFiles
+          .map(file => ({
+            name: file.path,
+            size: file.lfs?.size || file.size || 0,
+            quantization: this.extractQuantization(file.path),
+            downloadUrl: this.getDownloadUrl(modelId, file.path),
+            mmProjFile: this.findMatchingMMProj(file.path, mmProjFiles, modelId),
+          }))
+          .sort((a, b) => a.size - b.size);
+      });
     } catch (e) {
       // A timed-out/aborted request means the network is down — don't pay a second 5s on the
🤖 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 `@src/services/huggingface.ts` around lines 30 - 88, Refactor fetchWithTimeout
into a withTimeout wrapper that keeps the AbortController active until the
entire asynchronous operation completes, including response.json(). Update
fetchJson and getModelFiles to wrap both fetching and body parsing in this
timeout, while preserving existing non-OK handling, abort propagation, and
siblings fallback behavior.
src/screens/ModelsScreen/useTextModels.ts (1)

209-231: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Race condition in handleSelectModel state updates.

State updates are applied unconditionally after the async file fetch. If a user quickly navigates between different models, concurrent fetch responses can resolve out-of-order. This TOCTOU (Time-of-Check to Time-of-Use) race condition will overwrite modelFiles with stale data, causing the detail view to display and eventually download files belonging to a completely different model.

Guard the state updates to ensure only the latest active selection modifies the state.

🔒️ Proposed fix to ignore stale responses

(Note: ensure useRef is included in your react imports)

+  const fetchIdRef = useRef(0);
+
   const handleSelectModel = async (model: ModelInfo) => {
+    fetchIdRef.current += 1;
+    const currentFetchId = fetchIdRef.current;
+
     setSelectedModel(model); setIsLoadingFiles(true); setFilesLoadError(false);
     // Curated entries under the offgrid/ namespace (e.g. the synthetic LiteRT
     // parent) ship with their files baked into the ModelInfo — skip the
     // HuggingFace fetch and use them as-is. Real HF models always go through
     // the fetch path even when factories/mocks pre-populate model.files.
     if (model.id.startsWith('offgrid/') && model.files && model.files.length > 0) {
       setModelFiles(model.files);
       setIsLoadingFiles(false);
       return;
     }
     try {
       const files = await huggingFaceService.getModelFiles(model.id);
+      if (fetchIdRef.current !== currentFetchId) return;
       setModelFiles(files);
     } catch {
       // Fetch failed (offline / HF unreachable / timeout). Surface an inline retry state rather
       // than a transient modal — the detail view reads filesLoadError and offers Retry.
+      if (fetchIdRef.current !== currentFetchId) return;
       setModelFiles([]);
       setFilesLoadError(true);
     } finally {
+      if (fetchIdRef.current === currentFetchId) {
         setIsLoadingFiles(false);
+      }
     }
   };
🤖 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 `@src/screens/ModelsScreen/useTextModels.ts` around lines 209 - 231, Update
handleSelectModel to track the latest selection with a useRef-based request or
selection token, and verify that token before applying fetched files, errors, or
loading-state updates. Ensure stale responses from prior model selections cannot
modify modelFiles, filesLoadError, or loading state, while preserving the
existing offgrid fast path and current behavior for the active selection.
🧹 Nitpick comments (1)
src/types/index.ts (1)

25-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer top-level import type over inline imports.

Using a top-level type import is more idiomatic and improves readability compared to an inline import() type inside the interface definition.

♻️ Proposed refactor

Add the type import at the top of the file:

+import type { FitTier } from '../services/memoryBudget';

And update the field definition:

   /** Device-fit tier of this model's BEST (most-fitting) quant, for the browse fit chip. Set by the
    *  models VM from memoryBudget.fitTier; undefined until the file sizes are known. */
-  fitTier?: import('../services/memoryBudget').FitTier;
+  fitTier?: FitTier;
🤖 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 `@src/types/index.ts` around lines 25 - 27, Replace the inline import type used
by the fitTier property in the relevant interface with a top-level import type
for FitTier from the memoryBudget service, then reference FitTier directly in
the property declaration.
🤖 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 `@__tests__/unit/services/huggingface.test.ts`:
- Around line 596-604: Update the test around the custom global.fetch stub to
capture the original fetch implementation and restore it in a finally block,
ensuring restoration occurs whether the test passes, fails, or aborts.
Alternatively, replace the direct assignment with a jest.spyOn mock configured
with the existing behavior and ensure it is restored after the test.

In `@CLAUDE.md`:
- Around line 1-4: Update the references in batch9-diagnostics-debuglog.test.ts
and DEVICE_TEST_LOG.md that point to CLAUDE.md, replacing them with AGENTS.md so
debug-log and gate guidance directs contributors to the canonical source. Leave
unrelated repository instructions unchanged.

---

Outside diff comments:
In `@src/screens/ModelsScreen/useTextModels.ts`:
- Around line 209-231: Update handleSelectModel to track the latest selection
with a useRef-based request or selection token, and verify that token before
applying fetched files, errors, or loading-state updates. Ensure stale responses
from prior model selections cannot modify modelFiles, filesLoadError, or loading
state, while preserving the existing offgrid fast path and current behavior for
the active selection.

In `@src/services/huggingface.ts`:
- Around line 30-88: Refactor fetchWithTimeout into a withTimeout wrapper that
keeps the AbortController active until the entire asynchronous operation
completes, including response.json(). Update fetchJson and getModelFiles to wrap
both fetching and body parsing in this timeout, while preserving existing non-OK
handling, abort propagation, and siblings fallback behavior.

---

Nitpick comments:
In `@src/types/index.ts`:
- Around line 25-27: Replace the inline import type used by the fitTier property
in the relevant interface with a top-level import type for FitTier from the
memoryBudget service, then reference FitTier directly in the property
declaration.
🪄 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 Plus

Run ID: 4b441aab-6882-473d-8878-0f40b74e98ce

📥 Commits

Reviewing files that changed from the base of the PR and between 856bca1 and 9cacd2f.

📒 Files selected for processing (63)
  • AGENTS.md
  • CLAUDE.md
  • __tests__/integration/audio/voiceModeImageJourney.rendered.happy.test.tsx
  • __tests__/integration/audio/voiceModeResendEnhancedImage.rendered.redflow.test.tsx
  • __tests__/integration/audio/voiceModeResendImageRoutes.rendered.redflow.test.tsx
  • __tests__/integration/chat/voiceNoteChatModeEmptyTurn.redflow.test.ts
  • __tests__/integration/downloads/downloadedCountBadge.rendered.happy.test.tsx
  • __tests__/integration/downloads/iosImageStagingPurgedRedownloads.redflow.test.ts
  • __tests__/integration/downloads/iosImageStagingPurgedRedownloads.rendered.redflow.test.tsx
  • __tests__/integration/downloads/iosInterruptedNoFailedEntry.redflow.test.ts
  • __tests__/integration/downloads/iosTextRetryReissues.rendered.redflow.test.tsx
  • __tests__/integration/downloads/queuedDownloadsSurviveKill.redflow.test.ts
  • __tests__/integration/downloads/textRetryReissuesOnIosLostDownloadId.rendered.redflow.test.tsx
  • __tests__/integration/generation/enhancementNoThinking.rendered.redflow.test.tsx
  • __tests__/integration/generation/enhancementStreamingProgress.rendered.redflow.test.tsx
  • __tests__/integration/generation/generationFlow.test.ts
  • __tests__/integration/generation/imageGenerationFlow.test.ts
  • __tests__/integration/generation/queuedForceImagePreservesMode.rendered.redflow.test.tsx
  • __tests__/integration/generation/queuedSendFeedback.test.ts
  • __tests__/integration/generation/reloadRaceKeepsThinking.rendered.redflow.test.tsx
  • __tests__/integration/generation/remoteParallelTools.rendered.happy.test.tsx
  • __tests__/integration/generation/resendImageRoutes.rendered.redflow.test.tsx
  • __tests__/integration/generation/resendImageRoutesLlama.rendered.redflow.test.tsx
  • __tests__/integration/generation/resendImageTurnFallsBackToText.rendered.redflow.test.tsx
  • __tests__/integration/happy/imageBackends.happy.test.tsx
  • __tests__/integration/happy/imageIntentRouting.happy.test.tsx
  • __tests__/integration/happy/imageLightbox.happy.test.tsx
  • __tests__/integration/happy/imageModeToggle.happy.test.tsx
  • __tests__/integration/happy/imageOomCard.happy.test.tsx
  • __tests__/integration/happy/smartBudgeting.happy.test.tsx
  • __tests__/integration/happy/speakMessage.happy.test.tsx
  • __tests__/integration/happy/tools.happy.test.tsx
  • __tests__/integration/image/imageTunablesReadFreshFromStore.redflow.test.ts
  • __tests__/integration/models/activeModelService.test.ts
  • __tests__/integration/models/browseFitChipShowsLoadableModels.rendered.test.tsx
  • __tests__/integration/models/detailFilesFitHintUsesOwnedBudget.rendered.test.tsx
  • __tests__/integration/models/modelFilesLoadErrorShowsRetry.rendered.test.tsx
  • __tests__/integration/onboarding/deviceAnalysisDoesNotWaitForNetwork.rendered.test.tsx
  • __tests__/integration/onboarding/deviceCardShowsTotalRam.rendered.test.tsx
  • __tests__/integration/onboarding/spotlightFlowIntegration.test.ts
  • __tests__/integration/rag/embeddingFlow.test.ts
  • __tests__/integration/rag/ragFlow.test.ts
  • __tests__/integration/stores/chatStoreIntegration.test.ts
  • __tests__/rntl/screens/ModelDownloadScreen.test.tsx
  • __tests__/unit/services/huggingface.test.ts
  • __tests__/unit/services/modelManager.test.ts
  • android/app/src/main/java/ai/offgridmobile/download/DownloadManagerModule.kt
  • android/app/src/main/java/ai/offgridmobile/download/WorkerDownload.kt
  • fastlane/Fastfile
  • scripts/uat.sh
  • src/components/ModelCardContent.tsx
  • src/screens/DownloadManagerScreen/useVoiceDownloadItems.ts
  • src/screens/ModelDownloadScreen.tsx
  • src/screens/ModelsScreen/TextModelsTab.tsx
  • src/screens/ModelsScreen/index.tsx
  • src/screens/ModelsScreen/styles.ts
  • src/screens/ModelsScreen/useModelsScreen.ts
  • src/screens/ModelsScreen/useTextModels.ts
  • src/services/activeDownloadPersistence.ts
  • src/services/huggingface.ts
  • src/services/memoryBudget.ts
  • src/services/modelPreloader.ts
  • src/types/index.ts

Comment thread __tests__/unit/services/huggingface.test.ts
Comment thread CLAUDE.md
@sonarqubecloud

Copy link
Copy Markdown

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