feat(recorder): on-device recorder host + whisper transcription + chat/RAG - #433
Open
dishit-wednesday wants to merge 193 commits into
Open
feat(recorder): on-device recorder host + whisper transcription + chat/RAG#433dishit-wednesday wants to merge 193 commits into
dishit-wednesday wants to merge 193 commits into
Conversation
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…, stop, native log) Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…option (core) whisperService.transcribeFile gains offset/duration so a recording can be transcribed in chunks, and streams segments live via onNewSegments (cumulative result + per-segment t0/t1). loadModel takes useGpu/useFlashAttn/useCoreML and passes them to initWhisper (useCoreMLIos on iOS). Bumps the pro submodule to the chunked/resumable transcription experience. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…header Replaces the Settings bottom tab with a Memory tab. MemoryTabScreen renders the Pro recorder when it is registered (pro.activate) and a paywall otherwise, read reactively so unlocking Pro swaps it in without a restart - the tab shell stays in core, the recorder injects from the submodule. Settings becomes a pushed RootStack screen reached from a gear in the Home header. Updates nav types and the onboarding step that pointed at the old SettingsTab, and the SettingsScreen nav-prop type (no longer a tab). Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Adds a map-reduce summarizer (transcriptSummarizer) for text too large for the context window: split into context-sized chunks, summarize each, then combine (recursing if needed) - so the whole transcript is read instead of truncated to the tail like contextCompaction does. A Summarize action on a document/transcript attachment chip streams progress (part i/N, combining) into an assistant message, then the final summary. MediaAttachment gains recordingId + transcript range metadata, and a one-shot chatAttachmentInbox lets the Pro recorder hand a transcript to the composer across the navigation boundary. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Summarizing an oversized transcript used to show a static "part i of N" counter for minutes and only stream on the final combine pass, and the Summarize button was clipped invisible inside the 60x60 attachment chip. - generateWithMaxTokens gains an optional onToken callback. - transcriptSummarizer streams each map part as it is written (new 'combining' phase marks the final pass), so text flows from part 1, not part 3. - useSummarizeAttachment renders the live work (parts stack while streaming, then the combined answer streams over the top) on a 50ms flush so the JS thread is not saturated by per-token deep store updates. - Document/transcript chips get a larger 168x76 layout with a filled, visible Summarize button and a spinner+label busy state. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…er.rn 0.5.5) Hoist the segment-callback user_data to the dispatch-block scope so live segment streaming on the iOS file-transcribe path does not deref a dangling pointer; add an NSLog marker to verify the patch is compiled into a build. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…d project Thread per-chunk metadata (recordingId/startMs/eventTitle) through chunking, the SQLite store, and retrieval; add indexText() to index a raw transcript; seed a 'Recordings' project via ensureProject so recordings can be asked about. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Move the exact timestamp out of the system prompt (which busted llama.rn's prefix cache every turn) and onto the latest user message; keep only the stable date in the system prompt so the ~800-token system+tools prefix is reused. Add tool-loop diagnostic logging. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
… model Guard CoreML so it falls back to CPU when the per-model encoder asset is absent (was crashing on A12); force 'en' for English-only ggml-*.en models; add the small.en-tdrz model entry; re-enable iOS live segment streaming. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Used by transcribeChunked to log per-chunk memory footprint. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…tream fast On CPU-only low-RAM devices, reasoning/preamble burned the whole token budget before any summary streamed. Force the thinking channel off for internal generations (summarize, tool-selection), instruct the prompts to emit no preamble, strip any leaked control tokens, and cap each MAP chunk to ~1500 input tokens so each part prefills in under a minute and starts streaming sooner. Reduce/combine passes still use the full context budget. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Snapshot RAM before and after loadModel. On 4 GB iOS devices a large whisper model can push the app past the jetsam limit and the OS kills it mid-load, which presents as a crash; the before/after pair localizes a kill to load vs transcription. Fire-and-forget so it adds no await points to the load critical path. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The tab and paywall are about on-device recording/transcription, so call it Recorder. Tab label, testID (recorder-tab), and paywall title updated. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
loadModel gained an options param (CoreML/lang config); update the store tests to match the new call signature. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Turn the bare gating card into a feature list - always-on recording (Android), on-device transcription, summaries, calendar context - with a privacy line and the unlock CTA. Copy follows the brand voice (privacy-as-mechanism, no jargon). Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Points the pro submodule at the locket recorder feature set: continuous recorder, on-device transcription, calendar matching, summaries, knowledge-base sync, and the recording-detail/list UI polish. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Update tests left stale by intended code changes, and add coverage for new
behavior. No production code changed; each assertion was verified against the
current implementation (the source of truth), not weakened to pass.
- nav/onboarding: Memory tab renamed to Recorder (recorder-tab), and Settings
moved off the tab bar to a Home-header route, so STEP_TAB_MAP.exploredSettings
is 'Settings' and the 5th tab label/testID is Recorder.
- whisper: loadModel now forwards {useGpu,useFlashAttn,useCoreMLIos}; .en models
force language 'en' (plain .bin auto-detects).
- rag: schema gained a metadata column (4th CREATE = ALTER migration; 4th insert
bind = metadata, null when absent).
- generationToolLoop: the precise time-of-day note now rides on the latest user
message (keeps the system+tools prefix cacheable), not the system prompt.
- whisperService.branches: reset RNFS exists/stat mocks per test - clearAllMocks
does not drain the mockResolvedValueOnce queue, so onces leaked between tests.
Added: CoreML-encoder-missing -> CPU fallback guard, and RAG metadata JSON
serialization on the write path.
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
RNWhisper.mm's vadDetectSpeech/vadDetectSpeechFile decode the WAV to a malloc'd float PCM buffer and pass it to detectSpeech synchronously, but never free it. Over a long recording that leaks ~one slice of PCM per call and OOMs. Add free(data) after the synchronous detectSpeech consumes it, in both methods. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Add ungated controls to clear the cached/stored Pro license so the free -> activate flow can be re-tested on a release build (where the __DEV__ toggle is inert). The Keygen machine slot stays claimed (fingerprint persists), so re-pasting the key re-activates instantly. Marked TEMPORARY - re-gate behind __DEV__ or remove before shipping. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Point at the pro commits adding the recorder transcription/archive UI polish and the speech/noise/silence cleanup classifier. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The ChatInput render fn was 358 lines (limit 350) - the #431 lint blocker. Lift the send/stop/voice action button into a module-scope ActionButton component (derives colors/styles from the same hooks; behaviour identical to the previous inline ternary). Render fn now ~343 lines. 91/91 ChatInput tests pass, tsc clean. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The submodule URL still referenced the old wednesday-solutions/offgrid-pro repo. Point it at the canonical off-grid-ai/mobile-pro so CI and fresh clones fetch the pro recorder code from the right place. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Two repo-scoped agents: - pro-feature: keeps a Pro change inside the pro/ submodule and out of the public core repo, which is the rule most easily broken by accident. - device-verifier: build, install, pull the debug log, grep the state-machine traces. Runs one at a time, since there is one device. Also ignores skills-lock.json - local machine state, not project config. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Moves the pointer from 6b91f7b7 to 11a6db0, picking up 76 pro commits: the recorder becoming a projection of native truth, Parakeet as a managed STT model, speaker identification, the day feed rework, and the autonomous processing loop. Deliberately last. Three of those pro commits import core APIs added in this same branch - the STT model registry, the sttModelPrompt slot, remoteOnly summarization, and whisper's realtime subscription. Verified before bumping: all 60 core symbols pro HEAD imports resolve against core HEAD. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The comment claimed "pro/ ships its own suite run in the pro repo's CI". It does not. Pro's CI checks out core and runs THIS config, which ignores <rootDir>/pro/ - so nothing under pro/__tests__ has ever run there. That sentence is why eight VAD-declutter integration tests were moved out of core into pro (pro@87dd505, "de-leak from core") and silently stopped running. The comment now says where pro's suite actually runs, and warns that moving a test into pro/ takes it out of CI until pro's workflow invokes pro/jest.config.js. Comment only - no behaviour change. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Picks up pro@26d88b5, which adds pro/jest.config.js so pro's own 13 suites can run at all. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The test-only dynamic-import transform was scoped to pro/locket/screens/, so
`await import()` inside pro/locket/stores/recordingsStore.analyseDay still threw
"A dynamic import callback was invoked without --experimental-vm-modules" and took
down any rendered test that touched Analyse.
Widened to pro/locket/(screens|stores)/ - still narrow, for the reason the
original comment gives: a GLOBAL transform changes behaviour elsewhere and broke
loadProFeatures' `await import('@offgrid/pro')`.
Removes one whole error class. locketProcessingCard and locketSheetBusyGate were
each failing on two distinct errors; now only the pre-existing stale
`today-switcher` expectation remains, which is a test to update, not a crash.
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Four suites that mount the day feed. Two are green again; two are improved but still red, and the remaining cause is named below rather than left to be rediscovered. Three causes, all found by probing locketProcessingCard: 1. They navigated through a chooser that no longer exists - press `today-switcher`, wait for `switcher-recordings`, press it. pro@f8de024 deleted that pill when the feed became one timeline, so there is no step to perform. Removed. 2. No transcription model was seeded, so the feed rendered the first-run setup card instead of the timeline (LocketFeedScreen gates on `!!downloadedModelId`). 3. autoAnalyse now defaults ON. With an un-analysed clip on today it fires on mount, fails with 'no-model' because no LLM is seeded, and useLocketFeed routes to LocketTranscriptionSetup - which leaves the feed mounted but under aria-hidden, so the clip is in the tree and unreachable by query. That default flip is from this branch, so this one is ours. Both settings are now pinned in the fixtures rather than inherited: a test about card styling should not change meaning because a product default moved. locketProcessingCard also asserted a green left border (borderLeftWidth 3) that e60e565 replaced with a faint primary-tinted background, so that assertion never matched the code it was checking. Still red: locketAnalysisSync (5 cases) and locketRepairState (2 cases, a 10s waitFor hang with no missing-element error - a different failure class). Both are strictly closer than they were; neither was passing before this commit. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The eight rendered locket tests now live in pro/__tests__/integration/, where the feature they describe lives and where pro's own runner executes them. They were tracked here, and this repo is public: they walked through the feed card states, the pipeline percent, the busy gate, speaker turns and repair progress of an unshipped paid feature, screen by screen. Deleted here, added there in the same submodule bump. Around 40 pro-owned suites remain under __tests__/pro/ - the audio and MCP sets, covering features that have already shipped. Those are a separate migration and lower stakes. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
chore(locket): bump pro submodule to round 4
feat(locket): recorder round 2 — chat/RAG scoping, KB preview, whisper picker + pro round-3 bump
Removing 179 KB of real speaker embeddings from pro's history rewrote every commit that carried them, so the SHA this pointer held no longer exists. Points at pro's rebuilt tip. The fixture was 48 genuine 512-d voiceprints from a real conversation, including the enrolled user's. A deletion commit would have left the blob in history and still pushed it, so it was removed from every commit instead - the object is gone from the repository. The clustering test now generates its vectors. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
One conflict, in ios/OffgridMobile.xcodeproj/project.pbxproj: the beta release d032056 stamped CURRENT_PROJECT_VERSION 1784286784 in the same region where this branch added the widget extension target. Took the release's build number - a release stamp should win over our bump - and kept the widget target intact (24 OffgridRecorderWidget references still present, plutil clean). Also aligned the widget extension's own CURRENT_PROJECT_VERSION, which did not conflict and so was left at the older 1784267770. App Store Connect rejects a submission whose extension CFBundleVersion differs from the app's, so all four build configs now read 1784286784. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Resume was fully implemented and unreachable. `WorkerDownload.kt:127` already
measures the bytes at `destination` and sends `Range: bytes=$existingBytes-`, but
the staging path was `"${randomUUID}_$fileName"` - a fresh UUID per attempt. So a
retry never found the previous partial, the measured length was always 0, and every
attempt restarted from byte zero. A 652 MB Parakeet encoder failing at 600 MB threw
away all 600 MB.
The staging name is now derived from the URL, which buys both properties at once:
- the SAME url resolves to the same staging file, so a retry finds the partial
and resumes;
- a DIFFERENT url resolves elsewhere, so a partial from an earlier model version
is never resumed into. That is a real case, not a hypothetical - Parakeet v2 to
v3 changed the URL and kept the filename.
The filename stays on as a readable suffix, sanitised because it reaches the
filesystem.
Five tests cover it. Verified with `./gradlew :app:testDebugUnitTest --rerun-tasks`
(the task reports UP-TO-DATE otherwise and runs nothing): BUILD SUCCESSFUL.
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
A model shipped as several loose files declares `aggregated: true` in its transfer metadata and drives ONE user-visible row itself. Launch hydration did not know that, so after a restart every part came back as its own Download Manager entry: four rows titled by filename, quantization "Unknown", each with its own byte total, sitting beside the aggregate row that is supposed to be the single source of truth. `isAggregatedPart()` filters them in `getParentRows`, beside the existing mmproj-sidecar exclusion. Generalised rather than keyed to one model, because "several native transfers, one user-visible model" is not specific to Parakeet. Unparseable metadata is not treated as a reason to hide a download. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The scale had nothing between `body` (14) and `h2` (16, the screen-title token), so every list row sat at 14 whether it was a settings toggle or a paragraph someone actually reads - and reaching for `h2` would have given list rows the weight of a heading. `bodyLarge` is 15/21 for reading text: follow-ups, key points, the rows you read rather than scan past. The lineHeight matters more than the size here, because the body tokens carry none, and that is what made those lists feel cramped. Lands in core ahead of the pro screens that use it - they do not typecheck until the token exists. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Pro code was never typechecked on push. The root `tsconfig` EXCLUDES `pro/**` - correctly, because the public repo's CI does not check the submodule out - so the existing check only ever saw a pro file when a core file imported one. Pro screens are reached through the registry at runtime, not by an import, so every pro-only screen, hook and service was invisible to the gate. Two real type errors were sitting in the tree because of it. Pro has its own tsconfig, so run that too when the submodule is actually present. Guarded on `[ -f pro/tsconfig.json ]`, so a checkout without the submodule is unaffected. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Renaming a source left its knowledge-base document under the old title, so search hits and citations kept naming something the user had already renamed. A title change does not touch the chunks or their embeddings - only the display name has to follow. `renameDocumentByPath` updates that one column, found by the document's path, and no-ops when there is no such document. Re-embedding an hour-long transcript to change a title would be absurd. Core-only, no pro dependency. It lands ahead of the pro caller that needs it. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
`probe()` returned a bare boolean, so every failure looked identical - a denied iOS Local Network permission, a refused connection, a wrong probe path, and a host that simply is not there all collapsed to `false`. That is what made "the scan finds nothing" undiagnosable. It now reports a failure class alongside the latency, which separates those cases: a timeout at the full budget means nothing answered (or our own JS thread was too busy to service the socket), while a fast refusal means something is there and saying no. The scan's log line lifts two nested template literals into locals - same output, and it satisfies the lint rule that blocks the commit otherwise. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The `<domain-config>` listing localhost, 127.0.0.1 and 10.0.2.2 was redundant - the base-config already permits cleartext to every host, so Metro and LocalDream keep working without it. It was also harmful. The mere PRESENCE of any `<domain-config>` puts Android into strict hostname-aware TrustManager mode app-wide, which broke the WhisperKit SDK's model download because its ktor TLS validation is not hostname-aware. Removing the block is behaviour-neutral for cleartext and lets non-hostname-aware clients connect. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
`isFileTranscribing` is part of the eviction contract, and these cases are about scenarios where no file transcription is in flight - so the fake states that explicitly rather than leaving the veto undefined and letting the outcome depend on a default. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…it natively The native RAM heuristic was a redundant second guard, and it mis-fired. The UI slider already caps the budget to a per-device ceiling (12K on <=8GB RAM, 32K above) and warns past a safe threshold, and the JS load path refuses a load up front when the model will not fit free RAM - the same contract the llama.rn path runs under. On top of that, the native clamp crushed valid budgets to its 1024 floor on ordinary devices - an 8GB phone with a 3GB model, for instance - so a direct question or an attached transcript overflowed a context far smaller than the one the user had asked for. The configured budget is reported back to JS so compaction thresholds and the context-usage bar read the real value rather than the requested one. `LiteRTTokenBudgetTest` goes with it: it tested only the removed clamp, and nothing else references `clampTokenBudget`, `TOKEN_BUDGET_HEADROOM_MB` or `KV_MB_PER_TOKEN`. Verified `./gradlew :app:compileReleaseKotlin` clean. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Points core at pro's `feat/recorder-v3`: speaker roster and the picker sheet, one identify path, the parakeet download surface, share-as-text, the recorder tile, the liveness heartbeat, progressive transcription, and an interruptible clean-up. Gated before bumping, as in the previous round: every `@offgrid/core` symbol pro HEAD imports was checked against core HEAD - 167 locket files, zero missing. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Picks up the fix that wires `buildSpeechWindows` into the transcribe path. The helper had landed without its caller, so whisper was still tiling the whole file and the perf change was inert. Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
feat(recorder): round 6 core — download resume, LiteRT token budget, TLS fix, pro typecheck gate
feat(recorder): round 5 core — Live Activity widget, STT model registry, whisper eviction fix
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



TL;DR - Open-core host that wires in the private
pro/recorder submodule and shipsthe shared on-device transcription + retrieval engine it relies on. Recorder feature code
stays closed-source in
pro/; this PR is the public wiring + shared services. Pairs withoff-grid-ai/mobile-pro#11 (the submodule pointer references a commit on that pro branch, so
they land together).
flowchart TB subgraph pro["pro/ submodule (private, autolinked)"] rec["Recorder + AudioNormalizer"] end subgraph core["core (public host)"] slot["MemoryTabScreen<br/>paywall + registry slot"] ws["whisperService<br/>windowed transcribe + CoreML guard"] rag["RAG 'Recordings' KB<br/>metadata column + migration"] chat["chat: summarize attachment"] end slot -. "registered-screen slot" .-> rec rec -->|"16k mono WAV"| ws ws -->|"transcript + t0/t1 segments"| rag ws -->|"transcript"| chatWhat's in this PR
native autolink (
react-native.config.js); iOS background-audio mode..enlanguage forcing; iOS streaming-lifetime patch (whisper.rn+0.5.5).metadatacolumn + migration);summarize oversized transcript/document attachments.
.gitmodulesre-pointed to canonicaloff-grid-ai/mobile-pro;submodule pointer bumped to the merged pro commit.
How it works (expand for detail)
Transcription pipeline (whisper.cpp via whisper.rn)
transcribeFiledrives whisper.rn with an{offset, duration}window so long recordings transcribe in chunks and resume from acheckpoint (last finished chunk) instead of reloading the whole file. Live cumulative
segments surface through whisper.rn's
onNewSegments(nativenew_segment_callback),wired only when
nProcessors == 1(parallel decode cannot stream in order).(
transcribeFile-> ObjCtranscribeData) crashed becausenew_segment_callback'suser_datawas a stack struct freed before the callback fired. The patch hoists it so itoutlives the call; compiled into the iOS binary, so live segments work on both platforms.
ggml-<model>-encoder.mlmodelcbundle makes whisper.rn fail CoreML init, fall back toCPU, then crash at transcribe 0% on some devices (A12 / iPhone XS).
loadModelchecks theencoder asset exists and downgrades
useCoreMLIosto false when missing, so the toggle isa no-op instead of a crash.
.enmodels carry only English tokens; auto-detect(returns codes like "tg") or an explicit non-English pick makes whisper unstable on iOS
(crash at 0%, 762s for 13%, garbled).
transcribeFileforceslanguage: 'en'for anymodel whose catalogue
lang === 'en', falling back to the.en.binfilename convention.Retrieval (recordings knowledge base)
Transcripts index into a local RAG "Recordings" KB.
rag_chunksgains ametadata TEXTcolumn (recordingId, startMs, eventTitle, ...) via a guarded
ALTER TABLEmigration forolder installs; chunk metadata is JSON-serialized on write.
Integration / build
react-native.config.js(onlywhen the pro android/iOS sources are present), so closed-source code never leaks into the
public repo but builds when the submodule is checked out.
.gitmodulesre-pointed from the oldwednesday-solutions/offgrid-proto the canonicaloff-grid-ai/mobile-pro; submodule pointer bumped to the merged pro commit.Testing
npm run lint(0 errors),tsc --noEmit(clean), and the full jest suite pass; thepre-push hook ran them, no
--no-verifyused. Added CoreML-fallback + RAG-metadata coverage.Follow-ups (not blocking)
MemoryTabScreenlooks up the recorder by the concrete proscreen name; move it behind an abstract slot key so public never names a pro internal.
max-lines: deferred with a TODO; clearing it means moving modeldownload/management into its own module (~11 call sites across core + pro).
Reviewer's guide (commits by theme)
The branch is chronological; this groups the commits by area.
recorder/paywall) + move Settings to Home header; autolink pro native + pod install; iOS
background-audio mode.
segments + CoreML; CoreML asset guard +
.enforcing + tdrz; iOS streaming lifetimepatch; memory-snapshot logging;
whisperremote-server provider type.oversized attachments; stream summaries live.
off-grid-ai/mobile-pro.