Skip to content

feat(cot): DeepSeek-R1 CoT accordion, multimodal EXIF downsampling, TokenStorage hardening & test suite - #3

Merged
PrinceBad merged 3 commits into
mainfrom
feature/phase-5-ci-validation
Sep 17, 2026
Merged

PrinceBad merged 3 commits into
mainfrom
feature/phase-5-ci-validation

Conversation

@PrinceBad

Copy link
Copy Markdown
Owner

Summary

This PR hardens the offline Android client (com.example.auralocalai) across five key audit areas:

  1. DeepSeek-R1 Chain-of-Thought (CoT) Accordion: Collapsible reasoning UI with cancellation lifecycle handling, multi-block parsing, and 3-tier model scoping.
  2. Multimodal Memory & EXIF Matrix: Coarse-to-fine bitmap downsampling with 3-stream EXIF orientation/mirroring normalization and native bitmap recycling.
  3. TokenStorage Hardening: Unified secure Keystore token persistence, eliminating plaintext SharedPreferences write-path bypasses and migration loops.
  4. Startup ANR & Passive Download Policy: Backgrounded startup file migrations and prevented mid-chat model eviction upon download completion.
  5. Full Unit Test & Build Verification: Added pure Kotlin JUnit test suite ParseThinkBlocksTest; verified 42/42 unit tests passing and clean debug APK packaging.

Key Changes

1. Reasoning CoT Accordion & Cancellation Lifecycle (ChatScreen.kt)

  • Mid-Think Cancellation: Decoupled <think> tag presence from the generation lifecycle. When a user cancels mid-thought before </think> arrives, the header immediately transitions from "Thinking in progress…" to "Thinking (stopped)" in muted slate, collapsing by default rather than remaining frozen in live streaming mode.
  • Multi-Block Parsing: Replaced single-block string scans with an iterative while-loop supporting arbitrary N thought blocks joined with \n\n---\n\n dividers, aggregating interleaved responses cleanly into mainContent.
  • 3-Tier Model Scoping:
    • Catalog Non-Reasoning Models (qwen2.5-coder-3b, qwen-1.5b, gemma4-e2b): Strictly disabled from accordion parsing to prevent false-positive collapses when outputting XML/HTML tags or discussing prompt engineering.
    • Catalog Reasoning Models (deepseek-1.5b, qwq): Enabled by default.
    • Custom / Unknown Imported Models: Gated strictly on confirmed prefix: message.content.trimStart().startsWith("<think>"). Ambiguous prefixes (e.g. < or <th) are treated as provisional plain text until confirmed, avoiding false-positive locks on <html> or code blocks.
  • Stateless Scroll Recycling: Avoided ephemeral remember(message.id) latches in ChatBubble. Reasoning classification evaluates against the immutable message.content, surviving off-screen item disposal and recomposition when users scroll during long generations.
  • Legacy History Decoupling: Saved messages lacking modelId evaluate content prefix independently of uiState.activeModelId, ensuring switching models does not alter past DeepSeek chat presentation.

2. Multimodal Image Downsampling & EXIF Correction (LlmViewModel.kt, LlmInferenceEngine.kt)

  • Coarse-to-Fine Downsample: Added decodeSampledBitmap(context, uri, maxDimension = 1024) using 3 independent stream passes:
    1. Dimension probing (inJustDecodeBounds = true).
    2. EXIF orientation detection via ExifInterface(stream).
    3. Power-of-2 inSampleSize decode.
  • Unified Matrix Transform: Combines EXIF orientation (90°/180°/270°/flip/transpose) and fine scale resize in a single Bitmap.createBitmap pass, recycling intermediate allocations.
  • Native Memory Cleanup: Local vision bitmap is recycled in sendMessage's finally block. Decoupled from Coil UI image loading.

3. Keystore TokenStorage Hardening & Startup ANRs (LlmViewModel.kt)

  • Direct Keystore Persistence: Routed saveHfToken() and clearHfToken() asynchronously through tokenStorage.saveToken() and tokenStorage.clearToken() on Dispatchers.IO. Dropped raw app_settings plaintext writes.
  • Single Authoritative Reader: Removed plaintext read in init block 2, eliminating startup race conditions and retiring the accidental migrateTokenIfPresent() loop.
  • Startup ANR Prevention: Backgrounded migrateExistingModels() and refreshDownloadedModels() off the main thread into viewModelScope.launch(Dispatchers.IO).

4. Passive Download Completion Policy (LlmViewModel.kt)

  • Mid-chat download completions no longer forcibly evict the active loaded model. New models are auto-loaded only if modelState is ModelState.Unloaded || modelState is ModelState.Error; otherwise, a non-intrusive Toast is displayed ("$displayName downloaded. Ready in Model Settings.").

Test & Build Verification

1. Unit Tests (./gradlew.bat testDebugUnitTest)

  • Result: BUILD SUCCESSFUL in 30s
  • Total Tests: 42/42 passed (100% success rate)
    • ParseThinkBlocksTest (9 tests):
      • testSingleClosedThinkBlock: PASSED
      • testUnclosedThinkBlock_MidGenerationOrCancelled: PASSED
      • testMultipleThinkBlocks_Interleaved: PASSED
      • testMultipleThinkBlocks_TrailingUnclosed: PASSED
      • testNoThinkBlock_StandardOrCodingOutput: PASSED
      • testThinkBlock_EmbeddedInCode: PASSED
      • testEmptyAndWhitespaceThinkBlocks: PASSED
      • testLeadingWhitespaceBeforeThink: PASSED
      • testModelPresetReasoningResolution: PASSED
    • TokenStorageTest (5 tests): PASSED
    • ModelSafetyValidatorTest (10 tests): PASSED
    • ModelDownloaderTest (6 tests): PASSED
    • ChatRepositoryTest (6 tests): PASSED
    • TelemetryBenchmarkTest (6 tests): PASSED

2. Full Debug APK Build (./gradlew.bat assembleDebug)

  • Result: BUILD SUCCESSFUL in 19s (38 actionable tasks, 0 compiler/DEX errors).

Manual Verification Protocol

Test Scenario Action Expected Outcome
7A (Rear Camera) Attach portrait rear photo to Gemma 4 chat Image decodes upright with correct EXIF orientation; inference succeeds without OOM.
7B (Front Camera) Attach selfie front camera photo EXIF mirror/rotation is normalized; no upside-down inversion.
7C (Pre-rotated) Attach 90°/270° pre-rotated gallery photo Single-pass Matrix handles rotation + downsampling cleanly.
8A (Mid-Think Stop) Send prompt to DeepSeek-R1; tap Stop mid-thought Header switches to "Thinking (stopped)" in muted gray; animation stops cleanly.
8B (Coding Model) Ask Qwen 2.5 Coder to write XML containing <think> Output rendered as literal text/code; no accordion is created.
8C (Scroll Lifecycle) Scroll up and down in LazyColumn during generation Reasoning accordion retains folded state upon item disposal and recomposition.
8D (Legacy History) Switch active model to Qwen; view past DeepSeek chat Historical reasoning blocks remain formatted as "Thinking Process".

…tool, and reconcile 12GB RAM tier for Gemma 4 E4B
…, 3-tier scoping, and test suite

- Decouple unclosed tag parsing from generation lifecycle to show 'Thinking (stopped)' on cancellation
- Add 3-tier reasoning model scoping (catalog non-reasoning, catalog reasoning, and custom imported prefix check)
- Add live-streaming token prefix anticipation and remember(message.id) state latching
- Support arbitrary multi-block think sequences joined with dividers
- Add sampled bitmap decode with 3-stream EXIF matrix transform and vision memory recycling
- Route Hugging Face tokens strictly via TokenStorage on Dispatchers.IO and prevent passive session eviction
- Add ParseThinkBlocksTest JUnit test suite with 42/42 unit tests passing and verify assembleDebug
…ning detection to confirmed prefix

- Replace ambiguous partial match latch with confirmed tag check (trimmed.startsWith('<think>'))
- Prevent custom models outputting <html> or XML tags from locking permanently into reasoning mode
- Remove ephemeral remember(message.id) latch from ChatBubble, avoiding state resets on LazyColumn item disposal
- Retain confirmed startsWith('<think>') prefix gating on message.content across live streaming and scroll recycling
@PrinceBad
PrinceBad merged commit 5386614 into main Sep 17, 2026
1 check 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