Skip to content

chore(locket): catch feat/locket-pro up to dev (merge-conflict resolution)#559

Open
dishit-wednesday wants to merge 598 commits into
feat/locket-profrom
chore/locket-catchup-dev
Open

chore(locket): catch feat/locket-pro up to dev (merge-conflict resolution)#559
dishit-wednesday wants to merge 598 commits into
feat/locket-profrom
chore/locket-catchup-dev

Conversation

@dishit-wednesday

Copy link
Copy Markdown
Collaborator

What this is

Merge-conflict resolution only. This catches feat/locket-pro up to dev (595 commits behind, ~490 files) by merging dev in and resolving the conflicts. No new feature work — every change here is either dev's existing work landing verbatim or a conflict resolution.

How conflicts were resolved (dev = canonical; locket features preserved additively)

  • whisperService.ts / whisperModels.ts / whisperModelFiles.ts / whisperStore.ts — kept HEAD's locket transcription (chunked extractWavSlice, CoreML, diarize, vocabulary, WhisperBusyError, native logging) and folded in dev's disjoint deltas (activeDownloadModelId delete-scoping, B26 realtime→file fallback, file-op delegation). cleanTranscription single-sourced with the non-ASCII \p{L}\p{N} fix.
  • llm.ts — took dev's version + restored the generateWithMaxTokens streaming/GBNF superset the insights summarizer depends on (additive; dev's 2-arg callers unaffected).
  • activeModelService/index.ts — combined locket's text-only mmproj-skip with dev's GPU-aware overhead in the RAM estimate. Normal (non-text-only) loads are byte-identical to dev.
  • ChatInput (Attachments.tsx / index.tsx) — combined the two attachment features that landed separately: document Summarize + image tap-to-view.
  • remoteServer.ts — kept the 'whisper' provider arm (locket cloud transcribe).
  • constants / logger / llmToolGeneration / ChatScreenComponents — took dev's canonical versions.
  • whisperStore.refreshPresentModels — reconciled to not auto-adopt a model when none is active (restores dev's explicit-select semantics).
  • Deleted the two core whisper tests dev removed (pro-importing core tests).
  • jest.setup.ts — added mocks for the native deps locket introduced (@notifee/react-native, device-info power) so @offgrid/pro loads in tests.
  • pro submodule — bumped to the caught-up pro pin (see the paired pro-repo PR).

Gates

  • tsc --noEmit: clean. eslint: 0 errors. Full jest suite: 7831 passing; the intermittent single-suite failures are pre-existing parallelism flakiness (0 individual test failures, shifts between runs).

Not for merge yet

Coupled with the pro-repo catch-up PR (submodule pin). Raising for review.

alichherawalla and others added 30 commits July 12, 2026 11:06
…(DEV-B16); + remote harness

Reproduces the exact device bug on the correct surface (REMOTE, not local): a remote OpenAI-compatible
model streams reasoning_content (replayed VERBATIM from the captured LM Studio SSE at the XMLHttpRequest
boundary), the real provider + processDelta + chat render run on top, the ANSWER renders but the reasoning
is DISCARDED — because enableThinking = thinkingEnabled && provider.capabilities.supportsThinking is false
for remote (no thinking toggle), and processDelta gates reasoning accumulation on it. "Coming through fine
but not shown = a UI bug" (user).

Proven RED: precondition (answer renders) passes → transport works; the reasoning assertion fails →
red-for-the-right-reason. Falsified: removing the `&& ctx.thinkingEnabled` gate in processDelta greens it;
revert → red. Cracked by tracing the live flow with kept [REMOTE-SM]/[GEN-SM] logs (the send DID route
remote via isUsingRemoteProvider; it errored on an undefined provider config — my hand-construction bug,
now using the real createProviderForServerImpl registration).

New reusable remote harness (__tests__/harness/remoteHarness.ts): installRemoteStream replays a captured
SSE at the XHR boundary; installRemoteModel makes a remote model active the real connect-flow way (add
server → discover → register provider → select model), unloads the local model + clears activeModelId so
the dispatch routes remote. Unlocks T050/T051/OGAD (Ollama, gateway) with their captured shapes.

Kept: [REMOTE-SM] traces in generationService (isUsingRemoteProvider decision + generateResponse entry) and
a [GEN-SM] trace in generateWithCompactionRetry — permanent state-machine logs, __DEV__-gated like the rest.
CLAUDE.md: check /tests before writing any test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…6, proven)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o LM Studio T049)

Ground truth (ollama-raw-curl-proof part10 + user correction): Ollama's native /api/chat streams reasoning
in message.thinking, and handleOllamaChatLine routes it to onReasoning UNGATED — so it renders (device:
reasoning=211, user saw it). The opposite of the OpenAI-compat path (T049) which gates reasoning_content on
thinkingEnabled and drops it. Replays the captured Ollama NDJSON at the XHR boundary (endpoint :11434 → the
native path). Falsified: stop routing message.thinking → onReasoning turns it RED; revert → green. Proves
the LM Studio drop (T049) is provider-path-specific, not 'remote thinking is just off'.

remoteHarness: chunk the replayed body per-line so it drives both OpenAI SSE (data:…\n\n) and Ollama NDJSON
(…\n). T049 still red after the change (verified).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EOF
…oning-dropped is the real bug)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nking block mid-stream (DEV-B14)

B14 (device: reasoning dumped into the answer bubble until </think>, then reclassified) is FIXED: the render
parses <think> live during streaming. Proven by pausing the stream DEEP inside the still-open <think> and
asserting the reasoning is in thinking-block-content while the answer hasn't leaked it. Falsified against the
REAL lever — breaking parseThinkTags' <think> detection (messageContent.ts:258) makes the reasoning leak into
the answer → red; revert → green. (My first attempt broke the REASONING_DELIMITERS array, which parseThinkTags
does NOT use — a wrong-lever miss I caught before committing.)

Harness: llama fake gains char-by-char streaming + scriptCompletion({pauseAfter}) / releaseStream() to hold a
stream mid-token and assert the in-flight render — reusable for any streaming-render test. engine:'llama' to
match the device (gguf).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d, DEV-B14 fixed, proven)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…engine seam, proven non-vacuous

Audit confirmed FALSE GREEN: the old assertion rode litert.calls.generateRaw, which the main litert turn
never uses, so it stayed green even when the '[User wanted an image…]' marker was forced onto every send —
it guarded nothing. Rewritten to assert on the boundary that CARRIES the model prompt (litert sendMessage
text), where the marker would actually land if useChatGenerationActions.ts:448 fired. Non-vacuous checks
added (send reached the engine; exact prompt reached the model unmodified). Falsified: forcing
shouldRouteToImageGenerationFn=true makes line 448 fire → the marker appears in sendMessage → red; revert →
green. (Old test: green even when the marker was forced — proof it was theater.)

Harness: litert fake now records sendMessage args (calls.sendMessage) — the seam that carries the turn text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ot engine booleans

Audit confirmed FALSE GREEN: the old test asserted liteRTService/llmService.isModelLoaded(), which the
REDUNDANT unloadAllTextEngines() satisfies even with the residency swap deleted — so the accounting could
go stale/co-resident and it stayed green. Now asserts modelResidencyManager.getResidents() (the accounting
the residency manager owns): keeps BOTH states as a proven transition — after litert loads the one text
resident is 'lrt'; after llama loads it is 'llm' (litert evicted, never co-resident/stale). Falsified:
no-op'ing modelResidencyManager.register makes the accounting empty/stale → red; revert → green (the old
test stayed green through the same break — proof it was theater).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hisper (DEV-B1 generalised)

Generalises T023 (whisper-specific) to the real invariant: with text+image+whisper+tts+embedding resident,
the REAL activeModelService.ejectAll leaves whisper+tts+embedding resident (getResidents after = [embedding,
tts, whisper]) — the memory budget stays inflated after the user ejected everything. Root: index.ts:436-437
ejectAll → unloadAllModels + count=(textUnloaded+imageUnloaded); sidecars are never unloaded. Precondition
asserts all 5 types resident first (non-vacuous). The fix must free every resident type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eral, RED)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oves the black box) + proven test

The Currently Loaded section showed only the model name + DISK size — residency was a black box. Now it also
shows the RAM the model consumes (hardwareService.formatModelRam → ~X GB RAM), with testIDs
currently-loaded-model{,-name,-ram}. Rendered test validates it on the ACTUAL ModelSelectorModal UI, wired
with the real loaded path (llmService.getLoadedModelPath) reached via the real Home-picker load. Proven:
dropping formatModelRam from the meta turns it red; revert → green.

Part of finishing the residency cluster: residency is now VISIBLE in the UI (what's loaded + how much RAM),
not only in the getResidents() black box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…parity with text)

Same black-box removal for the image model: Currently Loaded now shows disk size + estimated RAM
(hardwareService.estimateImageModelRam → formatBytes) with testIDs currently-loaded-image-model{,-name,-ram},
mirroring the proven TextTab change. So the model selector shows how much memory EACH loaded model (text +
image) consumes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rModal test mock

The Currently-Loaded RAM feature calls hardwareService.formatModelRam / estimateImageModelRam; the existing
rntl mock lacked them (undefined → meta render crashed → 11 failures). Blast-radius miss on my part — added
the two mocked methods so the modal renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n X + Join Slack) above About

Compelling, on-brand (no exclamations/em-dashes): leads with the value — new features land there first,
subscribers get promo discounts, feedback shapes the roadmap; Slack = issues fixed fast, debug together,
early access. Emerald-accented section so it stands out above About. Opens x.com/alichherawalla and the
Slack invite via Linking. testIDs follow-on-x / join-slack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e About screen

Same two community links as the settings card, on the About screen (community section under Open Source),
same on-brand outcome-first copy. testIDs about-follow-on-x / about-join-slack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…line strings)

Single source of truth in sharePrompt.ts (next to GITHUB_URL); Settings + About import them instead of
duplicating the raw URLs. Also fixes an AnimatedListItem index collision in About.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion) + evictByKey [TDD]

TDD: the failing integration test (modelSelectorEjectResident) was written first, then the feature built to
green. The model selector now shows an 'In Memory' section listing EVERY resident (text/image/whisper/tts/
embedding) with its RAM and an individual Eject — so a user can free any single model (incl. the whisper
sidecar ejectAll leaks), leaving the others resident; ejected models lazy-reload on next use (ensureResident).

Adds modelResidencyManager.evictByKey(key): the missing capability — release() only forgot the accounting,
never called the model's unload(). evictByKey runs under the FIFO lock, calls the real unload, removes it.
Logs throughout ([MODEL-SM] user eject requested/done, [ModelResidency] evicted ... by user request).

Proven: RED without the feature; falsified — no-op'ing evictByKey turns it red (eject can't free whisper);
revert → green. No regression (ModelSelectorModal rntl 46 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on next send (proven transition)

Real path: load text (Home picker) → user ejects (evictByKey) → not resident → send → answer renders AND
text is resident again. The false→true residency transition across the send is the reload proof (the answer
alone could come from the engine fake; the re-registration is what proves ensureResident reloaded it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ejectAll unloaded only text+image and counted (textUnloaded?1:0)+(imageUnloaded?1:0);
whisper/tts/embedding sidecars registered with modelResidencyManager leaked and kept
charging the memory budget after the user ejected everything. Loop the remaining
residents through modelResidencyManager.evictByKey after unloadAllModels so their real
unload runs (they lazy-reload on next use).

Flips two device-grounded reds green, both via real interactions over device-boundary
fakes and asserted on the UI residents surface:
- ejectAllLeavesWhisper (T023): real download-gesture arrival -> whisper resident ->
  ejectAll -> probe-residents no longer contains whisper.
- ejectAllUnloadsEveryType (T023b): image+whisper co-resident via real load paths ->
  ejectAll -> getResidents() empty. Rebuilt from register() shortcuts to real interactions.
Both falsified: removing the eviction loop leaves the sidecar resident -> red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pro submodule 521ba200 -> 042b4bc2 (removes the Pro Tools settings section
registration; Settings no longer surfaces it per product decision).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
whisperStore.downloadModel auto-loaded the STT model resident the instant the download
finished (await get().loadModel()), leaving a phantom ~1.5GB whisper in RAM the user
never transcribed with. On device makeRoomFor then counted that squatter against a
heavier text load (fits=true into 1.6GB real free RAM) -> thrash/OOM ('this gemma4 e2b
is struggling... some coresident bullshit in the ram').

A download only puts the file on disk. Loading is owned by the transcribe path: whisper
lazy-loads on demand via startRecording -> ensureWhisperForTranscription, and is warmed
fits-gated at launch by modelPreloader.preloadStt -- exactly the deferred-loading model
every other model follows, and what useWhisperTranscription's own NOTE already documents.

Flips T022 (whisperResidentOnDownload.rendered.redflow) green via the real download
gesture + UI residents probe. Falsify: restore the auto-load -> whisper resident -> red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oval

Nets the new-feature UI work added this session that previously had no behavioral test:
- Settings 'Stay in the loop' card: renders Follow-on-X + Join-Slack; each tap hands the OS
  the exact shared URL constant (Linking.openURL(FOLLOW_X_URL/SLACK_INVITE_URL)).
- Settings: Pro Tools row no longer renders (absence guard, mirrors the Voice/TTS removal guard).
- New AboutScreen.test.tsx: the same Follow-on-X + Join-Slack links render and open the right URLs.

Asserts against the imported shared constants (test-side DRY), not re-hardcoded strings. All
falsified: wrong URL -> the openURL assert fails; re-added Pro Tools -> the absence guard fails;
revert -> green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gging infra

Characterization test (green): on a <=6GB device, sending a text turn frees the idle whisper
sidecar (reclaimSttForGeneration) while the text model stays and the reply renders. Real
Home->Chat, whisper co-resident via the real download+select, RAM dropped to 6GB, real send.
Falsified: keep the device roomy (>6GB) -> whisper stays -> red.

Getting there surfaced real infra + coupling issues, fixed here:
- logger.ts: when DEBUG_LOGS is set under jest, mirror every logger.* to process.stderr (jest
  mocks console.*). Now 'DEBUG_LOGS=1 npx jest <file>' surfaces ALL source [X-SM] traces with no
  per-test patching. Opt-in, quiet by default.
- ChatInput: [COMPOSER-SM] handleSend trace (matches the [GEN-SM]/[MODEL-SM] convention; on-device
  visible via the debug log sink).
- chatHarness.send(): drive the composer's real onChangeText handler + invoke the send button's
  resolved onPress. Once a whisper/STT model is present, RTL fireEvent.changeText/press stop reaching
  the composer (the subtree reshapes with voice availability) -- a ChatInput coupling smell (text-send
  readiness should not depend on voice; feeds the send-piping consolidation). Also dismiss the
  onboarding spotlight in setup so its AttachStep never intercepts gestures. Blast radius: 13 tests
  across 6 chat suites still green.
- DEVICE_TEST_FINDINGS: B1 fix traceability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y UI, not getResidents()

The reclaim result now rides on the real user-facing indicator: open the ModelSelectorModal and
assert the whisper row is gone from the 'In Memory' section while the text row remains — the feature
built to remove the residency black box. Replaces the service-level expect(getResidents()). Falsified:
roomy device -> whisper stays listed -> red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ict/budget × modalities × text/voice)

Area 3 was text/image + eject-centric. Adds the missing cells, all validated through the model
selector In Memory UI (the black-box-removing feature), not getResidents():
- T111 ✅ sttReclaimedOnSend — text turn reclaims idle whisper on a tight device
- T112 ✅ modelSelectorEjectResident — per-model eject from In Memory
- T113 ✅ modelSelectorShowsLoadedRam — loaded model + RAM shown
- T114 ✅ lazyReloadAfterEject — ejected model reloads on next send
- T115 ❌ voice-note send reclaims STT after transcription (voice twin of T111)
- T116 ❌ allowed co-residence: text + whisper on a roomy device
- T117 ❌ auto-eviction on OS memory-warning (UI-flow version)
- T118 ❌ embedding sidecar lazy-loads + co-resides on first RAG query
- T119 ❌ whisper blocked->free->retry when a heavy owns RAM
- T120 ❌ TTS co-residence during a voice turn

Counts: 110->120 cases, 52->56 automated, 33->39 to-write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Roomy device: a heavy text model and the whisper STT sidecar co-reside — the single-model rule
evicts heavies for each other (T026) but not a reclaimable sidecar. Validated through the model
selector In Memory section: text-only -> load whisper -> BOTH resident-item-text and
resident-item-whisper listed. Falsified: skip the whisper load -> whisper not listed -> red.

Also scaffolds docs/RESIDENCY_TEST_MISMATCHES.md for logging any device-evidence mismatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
modelResidencyManager registers a real AppState('memoryWarning') listener that reclaims idle
sidecars (whisper/tts/embedding) on OS pressure while keeping the active heavy. The RN jest mock
swallows the listener, so nativeBoundary now installs a capturing AppState + exposes
emitMemoryWarning() (OS event -> the app's real listener -> real handleMemoryWarning). Validated
through the model selector In Memory section: fire the warning -> whisper row gone, text row stays.
Falsified: no warning -> whisper stays -> red. Blast radius: 9 chat/memory tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…emory UI)

Voice twin of T111: enter voice mode (real gesture), record a voice note + release to send (real
transcribeFile -> onTranscript -> send). On a <=6GB device the send goes through the same
handleSendFn -> reclaimSttForGeneration path, freeing idle whisper for the LLM turn (confirmed via
[ModelResidency] reclaiming idle STT trace). Reply is an AUDIO bubble (spoken via TTS), so asserted
via audio-bubble-<id> + the reply content, then the reclaim via the model selector In Memory section
(whisper gone, text stays). Falsified: roomy device -> whisper stays -> red.

Notes for the harness quirks hit: setupWhisperModel must precede render (voice tests' order); a
ModelSelectorModal cannot be rendered mid-voice-turn (disrupts the record state) so the In Memory
check runs AFTER voiceSend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alichherawalla and others added 25 commits July 15, 2026 07:24
…tType

ios-device.sh (used to pull the on-device offgrid-debug.log) selected the target
by connectionProperties.transportType != "None". On modern setups the phone is
reached over the CoreDevice network tunnel, so transportType reads "None" even
for a fully-connected wired device, and the old filter matched nothing. Select on
connectionProperties.tunnelState == "connected" instead — the field devicectl
itself uses to decide reachability (its own list-devices prints State=connected
for exactly these). Still never exits non-zero when no device is found, so the
friendly guard reports it rather than set -e aborting mid-detection.
…-detection

chore(scripts): detect wired iOS device via tunnelState, not transportType
Update the Pro badge from '$50 lifetime' to '$69 lifetime and $49 annual' to
reflect the current two-tier pricing.
…as, actions)

Align the Pro block with the site's aspirational framing (website/mobile.md):
voice mode (Kokoro TTS), custom personas, draft-then-approve (Calendar/email/MCP),
and Sync landing through July. Fix stale pricing ($50 -> $69 lifetime / $49
annual, matching _data/pricing.yml) and drop the licensing sentence.
…t no-op)

Device 2026-07-15: an app-kill mid-download (7 concurrent multi-GB models, then
'network connection lost') left failed text-download cards whose STORE entry had
lost its downloadId, while the native row kept it. retry() bailed at
`if (!entry?.downloadId) return` before reaching the iOS re-issue path, so
downloadModelBackground never fired — every retry tap was a silent no-op (the log
shows ~40 retry dispatches, 0 re-downloads).

The downloadId is only needed on Android, which resumes the WorkManager job in
place. iOS re-issues from scratch (restartIosTextDownload rebuilds from the
entry's modelId/fileName/metadata) and never uses downloadId. Move the downloadId
guard into the Android branch; iOS retry now only requires the entry to exist.

Device-verified working. Regression: 169 download/models tests pass, tsc clean.
When the 3-download concurrency cap is full, downloadModelBackground blocks until
a slot frees, so a retried item sat showing 'failed' with no signal it was
actually queued (the '4th item won't retry' report — it was queued, not dead).
Mark the entry 'pending' the instant retry runs, mirroring Android. Device-verified.
The icon badge counted only isActiveStatus (downloading + queued) and dropped
failed, so a failed/retriable download was invisible — no signal to open the
manager. Add a shared isFailedStatus; the badge now counts active + failed and
the Download Manager header surfaces a matching 'N failed' count so the two
agree (T001 updated: badge == downloading + queued + failed).
…de hang)

Device 2026-07-15 (P0): in VOICE MODE, tapping + to attach a photo hung the app
completely — could not even navigate back. The picker works fine in chat mode, so
the differentiator is voice mode's active iOS AVAudioSession (playback, for TTS):
presenting the native camera/photo picker on top of it wedges the main thread on
the audio-route handoff.

Add audioSessionManager.deactivate() (setAudioSessionActivity(false)) and call it
before launchImageLibrary/launchCamera. TTS/recording re-assert the session on
their next call (ensurePlayback re-activates every call), so no explicit restore.

iOS-only + Android-safe: audioSessionManager is entirely Platform.OS==='ios'
(no-op on Android), so the Android picker path is byte-for-byte unchanged.
tsc clean; 108 audio-session/chat-input tests pass. Needs on-device verification
(a native main-thread hang can't be reproduced in jest).
… wedge)

Device 2026-07-15 (P1): tapping Save in the fullscreen image viewer left the chat
input unresponsive (nav still worked) — ONLY on Save. The viewer is a <Modal> and
the 'Image Saved' confirmation is an AppSheet (also a modal); iOS cannot present a
second modal on top of the open viewer, so the pair wedges the UI. Close the viewer
first, then save + show the alert on the next tick after it fades out. Android has
no nested-modal conflict, so this is harmless there. tsc clean; 13 tests pass.
Debug config now uses ai.offgridmobile.dev so a dev build installs alongside the
App Store / TestFlight build (Release stays ai.offgridmobile, so releases and their
provisioning are untouched). Update the CLAUDE.md log-pull to target the .dev
container and to read the device UDID from `devicectl list devices` instead of a
stale hardcoded one — the mismatch is what broke the earlier log pulls.
…ed id

The script hardcoded BUNDLE_ID=ai.offgridmobile for the launch, so after Debug got
the .dev suffix it installed ai.offgridmobile.dev but launched the old
ai.offgridmobile — the non-.dev app kept coming up. Read CFBundleIdentifier from the
built .app's Info.plist so launch always matches install (verified: resolves to
ai.offgridmobile.dev).
Core half of the voice-mode picker-hang fix: pass handleAttachPress to the audioMode
slot as onAttachPress (adds a [COMPOSER-SM] log), and bump the pro submodule to
fix/voice-attach-reroute (3599b5b1) which uses it for the + button. iOS → native
ActionSheetIOS; Android → the same attachPicker popover as before (unchanged).
…ed feedback

Guards the two device fixes: (1) retry re-issues even when the store entry lost its
downloadId (app-kill) — verified RED with the old `!entry?.downloadId` guard; and
(2) a retried item leaves the failed state (queued feedback). Mounts the real
DownloadManagerScreen over the download+fs fake, taps the failed-retry-button.
…er-dismiss delay

- pickFromLibrary/pickFromCamera re-assert ensurePlayback() in a finally, so cancelling the
  picker (which deactivated the session) doesn't leave voice mode muted. iOS-only no-op on Android.
- Extract the 350ms save-after-viewer-dismiss delay to a documented VIEWER_FADE_OUT_MS with the
  modal-fade coupling spelled out.
… UDID

- Remove the mockist AudioModeLayout '+' test (jest.fn + toHaveBeenCalled) per our testing
  doctrine; the reroute is device-verified and the native sheet can't be unit-asserted.
- CLAUDE.md log-pull now reads hardwareProperties.udid from `devicectl --json-output` instead
  of awk on the human-readable table (the last column is the model, not the UDID).
…ailed-badge

fix(ios): download retry/badge, voice-mode + photo-picker hangs, .dev debug bundle
uat.sh's `git fetch --tags` recursed into the pro submodule and failed on a stale
pro pointer referenced by old core tag history but no longer on pro's remote
("not our ref") — blocking every build. The tag refresh only needs CORE tags to
pick the next beta number; the submodule is already at its pinned commit. Add
--no-recurse-submodules.
fastlane regenerates fastlane/README.md on every run, so after any build the tree is
'dirty' and the next run refuses to start. It is not build source — exclude it from
the porcelain check.
fix(uat): unblock the release build (tag-fetch recursion + fastlane dirty-check)
…lake

Raise the per-step waitFor budgets (4s->10s) and the overall test timeout (30s->60s).
This test flaked once on a loaded pro CI runner (residency settled just past the 4s
window; passed on core CI + locally with the same config). Behaviour-neutral — only
more headroom for the async settling. Pairs with the stranded beta-version bump.
…rsion-sync

chore: sync shipped beta version to main + harden residency test flake
Catch feat/locket-pro up to dev (595 commits, ~490 files). Conflict
resolutions (dev = canonical current; locket features preserved additively):

- whisperService.ts: HEAD base (locket transcribeFile chunking/extractWavSlice,
  CoreML, diarize, vocabulary, WhisperBusyError, native logging) + dev's disjoint
  deltas grafted in: activeDownloadModelId delete-scoping, B26 realtime→file
  fallback, and file-ops delegated to whisperModelFiles (dev's extraction).
- whisperModels.ts: HEAD's rich catalogue (WhisperModel interface, CoreML urls,
  tdrz) + single-sourced cleanTranscription with the non-ASCII \p{L}\p{N} fix.
- llm.ts: dev's version + HEAD's generateWithMaxTokens superset (streaming/GBNF/
  repeatPenalty) the insights summarizer depends on.
- activeModelService: combined textOnly mmproj-skip (locket) + GPU-aware overhead
  (dev) in the RAM estimate.
- ChatInput Attachments: combined document Summarize (locket) + image tap-to-view (dev).
- remoteServer: kept the 'whisper' provider arm (locket cloud transcribe).
- constants/logger/ChatScreenComponents/llmToolGeneration: took dev's canonical
  versions (DevGrammar harness dropped from the chat path).
- whisperStore: kept locket additions; reconciled refreshPresentModels to not
  auto-adopt a model when none is active (restores dev's explicit-select semantics).
- Deleted the two core whisper tests dev removed (pro-importing core tests).

tsc + lint green. Remaining test failures are all pro-submodule-coupled (TTS/audio
tests expecting pro-main's newer Kokoro engine) — resolved by the coupled pro
catch-up + gitlink bump, tracked as the follow-up step.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…deps in tests

Point the pro submodule at the catch-up merge (recorder branch feat/continuous-smart-recorder
merged with core-dev's pinned pro 3599b5b) so the pin carries BOTH the locket recorder work
AND dev's newer TTS/Kokoro audio engine. Without this the core pin lagged pro-main and every
voice-mode/TTS test failed.

The locket recorder introduced native deps the core test env didn't mock, so requiring
@offgrid/pro threw during pro activation (no slots registered → EngineBridge never mounts →
TTS isReady never fires → 13 voice/TTS tests failed). Add the mocks:
- @notifee/react-native (meeting reminders): full method surface + real enum values.
- react-native-device-info: usePowerState hook + getPowerState (recorder power gating).

Restore the onAttachPress prop in the AudioModeLayout test now that the caught-up pro has it.

Full core suite green (7831 passed); the intermittent single-suite failures are pre-existing
parallelism flakiness (0 individual test failures, shifts between runs).

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
@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 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 04d86817-f792-4787-95da-6353f0b69bee

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/locket-catchup-dev

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.

The previous bump targeted the pin core happened to hold (10f49a18), which was 13
commits behind the live recorder branch (feat/continuous-smart-recorder = 5b5921cc)
— all recent locket polish (LoudnessEnhancer, header search, honest insights load,
single-source RAM budget, hide-no-speech, unified feed, etc.). Repoint the submodule
to the catch-up built on 5b5921cc + core-dev's pro (3599b5b), so the merge includes
every recent locket commit, not a stale snapshot.

tsc clean; voice/TTS/whisper suites green against the new pin.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
@dishit-wednesday

Copy link
Copy Markdown
Collaborator Author

Paired pro-repo catch-up PR: off-grid-ai/mobile-pro#24 (submodule pin points at its tip). Review/merge them together.

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

2 participants