Skip to content

feat(recorder): round 5 core — Live Activity widget, STT model registry, whisper eviction fix - #619

Merged
dishit-wednesday merged 43 commits into
feat/locket-profrom
feat/recorder-v2
Aug 3, 2026
Merged

dishit-wednesday merged 43 commits into
feat/locket-profrom
feat/recorder-v2

Conversation

@dishit-wednesday

Copy link
Copy Markdown
Contributor

Core half of recorder round 5, on top of fix/locket-round-3. 26 commits.

Pairs with off-grid-ai/mobile-pro#44 — three commits here exist only to satisfy pro's half,
and the submodule bump is deliberately last.

iOS Live Activity + recorder tile (widget extension)

A recording shows on the Lock Screen and Dynamic Island with an elapsed clock and a Stop button
that ends it without opening the app; the Home Screen tile flips emerald/red and starts or
stops in the background.

The ActivityKit Swift is here rather than in pro/ios/ on purpose: the bridge must construct
Activity<RecorderActivityAttributes>, and that type has to be a member of the widget extension
target — a CocoaPods module cannot join an app extension without pulling React in. The pro side
still owns every product decision. The project.pbxproj, the App Group entitlement and
NSSupportsLiveActivities must land together or the iOS build is broken.

Device-verified on an iPhone XS: Stop from the Lock Screen ends the recording without the app
opening. Dynamic Island layouts are not verified — an XS has no pill.

An STT model registry, so a pro model can be app-managed

The Models screen and Download Manager only knew about whisper, so Parakeet's cancel/retry/delete
were routed to whisper and its progress never appeared. A model now registers itself with an id,
size, download and delete; pro registers itself exactly as ttsProvider already does, so core
still never imports pro
. Adding a third engine needs no change here.

Three fixes worth reading on their own

  • Do not evict whisper mid-transcription. An iOS memory warning evicted the LRU model. If
    that was whisper while a file transcription ran, the job was cancelled to avoid a
    use-after-free — whisper.rn reports it as Code: -999 — and the clip surfaced as "Failed to
    transcribe" through no fault of its own. The model was loaded for that job.
  • remoteOnly summarization. An unattended summary that quietly falls back to on-device is a
    jetsam risk while the mic assertion keeps the app alive. It now runs remote or fails.
  • iOS Local Network permission. Metro would not connect on a physical device: iOS refuses to
    show the permission alert during didFinishLaunchingWithOptions, which is exactly when RN
    issues its packager probe. Re-issued once a foreground UI exists.

Android build

assembleReleaseLocket produces a production-like build with a .locket applicationId suffix so
it installs alongside the normal app instead of replacing it. Plus arm64-only packaging
(~255 MB saved) and a note recording why onnxruntime-android must not be excluded — removing it
fails at runtime with NoClassDefFoundError, not at build time, which is exactly what a future
cleanup would delete confidently.

Tests

15 pro-owned locket suites were removed from this public repo and moved into pro, where the
feature lives and where pro's new runner executes them. They described an unshipped paid feature
screen by screen. Around 32 pro-owned suites remain here (audio and MCP — shipped features); that
is a separate migration.

babel.config.js's test-only dynamic-import transform was widened from pro/locket/screens/ to
also cover stores/ — await import() in recordingsStore.analyseDay was throwing and taking
down any rendered test that touched Analyse. Still narrow, for the reason the original comment
gives.

Deliberately NOT included

Four release landmines are held out of this branch and remain uncommitted locally:
App.tsx and debugLogFile.ts un-gated from __DEV__, SettingsScreen.tsx's dev button group
exposed in release, and DEV_UNLOCK_PRO = true which defeats the paywall. Also held:
LiteRTModule.kt (unclaimed, and it deletes a test) and network_security_config.xml.

Ordering

Pro must merge first — the submodule bump at the end of this branch points at pro#44's tip.

dishit-wednesday and others added 30 commits July 27, 2026 19:29
On-device STT engine test-bed dependency (cactus parakeet/moonshine/whisper).

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>
A local or BrowserStack build run drops the artifacts in the repo root, where
they were untracked but NOT ignored - one `git add .` away from being committed
into a .git that is already about 9 GB. The APK from the last run was 803 MB and
the IPA 75 MB.

Also ignores the two large sherpa binaries that are placed at build time rather
than tracked.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
… stay

Three packaging changes and two notes that exist to stop someone undoing them.

- arm64-only: drops the x86, x86_64 and armeabi-v7a copies of every native lib,
  including those from dependency AARs that ndk.abiFilters cannot reach. Saves
  roughly 255 MB. Every flagship and every BrowserStack real device is arm64-v8a.
  NOTE: this strips x86 from debug too, so x86 EMULATORS will not run this build -
  use a real arm64 device, or comment the excludes out for emulator work.
- pickFirst on libc++_shared.so, shipped by about nine native modules
  (reanimated, llama.rn, whisper.rn, audio-api, executorch, gesture-handler,
  screens, worklets, op-sqlite). Resolves the duplicate at merge time.
- noCompress 'zip' for the bundled Cactus STT model zips: already compressed, so
  storing them keeps the build fast and lets unzipAssets read them directly.

The two notes are the point of this commit as much as the code. Do NOT re-add an
exclude for com.microsoft.onnxruntime:onnxruntime-android - it existed only while
the -qnn AAR (a superset) was on the classpath for the dropped Whisper-NPU PoC.
With that gone this artifact is the ONLY thing providing ai.onnxruntime.*, which
pro's SileroVad needs for the recorder's live VAD gate. Excluding it fails at
RUNTIME with NoClassDefFoundError, not at build time - which is exactly the kind
of thing a future cleanup pass would delete confidently.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…al one

Testing the recorder meant replacing the build already on the device, so the
tester lost whatever state they had. `assembleReleaseLocket` produces a
release-configured build - JS bundled, minified, production-like - with an
`.locket` applicationId suffix and its own launcher name, so it sits next to
ai.offgridmobile instead of over it.

The launcher label goes through a manifest placeholder rather than a second
strings resource: the default keeps the real name, and only this build type
overrides it.

Falls back to the debug signing config when no release keystore is present, so it
builds on a machine without the signing secrets.

npm scripts for build / install / launch, since the gradle task name and the
suffixed applicationId are both easy to get wrong by hand.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
A recording now shows on the Lock Screen (and the Dynamic Island on 14 Pro and
up) with an elapsed clock and a Stop button that stops the recorder WITHOUT
opening the app. Plus a Home/Lock Screen tile that reflects real state: emerald
circle idle, red square recording, tapping it starts or stops in the background.

Why this Swift lives in core rather than pro/ios: the bridge has to construct
Activity<RecorderActivityAttributes>, and that type must be a member of the
widget extension target. A CocoaPods module cannot join an app extension without
dragging React in. The pro side still owns every product decision - when to
start, what the status line says. This is a deliberate placement, not an
oversight.

The three pieces must ship together and they do: project.pbxproj references the
files, the entitlements declare the group.ai.offgridmobile App Group that
RecorderSnapshot writes through, and Info.plist declares NSSupportsLiveActivities.
Any one without the others is a broken iOS build.

Two findings recorded in the code because they cost real time:

- A plain AppIntent with openAppWhenRun=false runs in the WIDGET EXTENSION's
  process, so its NotificationCenter post never reaches the app - tested on device,
  completely silent, no error. StopRecordingIntent works only because
  LiveActivityIntent is documented to run in the app's process. The comment in
  StartRecordingIntent.swift exists so the next person does not retry the flag.
- The notification NAME is the cross-target contract. The intents are here in the
  app target; the observer is in pro's ContinuousRecorderModule, which cannot
  import app-target Swift. If the two strings drift, the buttons silently do
  nothing.

RecorderLiveActivityPreviews.swift is wholly inside #if DEBUG - preview
scaffolding, ships nothing. The NSLog calls are permanent: NSLog was the only way
to observe ActivityKit, since it does not reach offgrid-debug.log.

Android parity is a declared gap, not a missing implementation: these are iOS-only
native modules and the JS services guard on Platform.OS, following the existing
recordingNotification pattern.

Device-verified on an iPhone XS: started, Stop from the Lock Screen ended the
recording without opening the app, tile state flipped. Dynamic Island layouts are
NOT verified - an XS has no pill; they render in the Xcode canvas only.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…sh script

On a physical iPhone, Metro would not connect. The cause is that iOS refuses to
present the Local Network permission alert during
didFinishLaunchingWithOptions - which is exactly when React Native issues its
packager probe. So the probe fails silently, the user is never asked, and the
bundler looks unreachable.

warmUpPackagerLocalNetwork re-issues the probe once a foreground UI exists, which
is when iOS will actually show the prompt. Dev + iOS only.

scripts/ios-refresh-js.sh rebuilds just the JS bundle into an installed app, so
iterating on JS does not need a full Xcode build.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
Follows the dependency additions: Cactus + react-native-nitro-modules, and
ZIPFoundation for reading the bundled model zips. Also picks up the OffgridPro
pod's own change.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The store kept the last 500 lines by copying the whole array on every append -
so every logger call allocated a 500-element array. With the state-machine traces
on, that is thousands of copies a minute, on the JS thread.

It now appends in place and trims only when the cap is exceeded.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
…udio boundary

Adding cactus-react-native broke every core test that imports @offgrid/pro:
requiring it throws "Failed to get NitroModules" at import time, so the suites
crashed on collection rather than failing a test. jest.setup mocks nitro-modules
and cactus-react-native, and jest.config adds cactus-react-native to
transformIgnorePatterns. Both are needed - half of it still crashes.

These are stubs for external npm packages, not for our own code, so they are not
a mockist test and not a pro-into-core leak.

nativeBoundary gains ggml Silero VAD scripting and an AudioNormalizer fake (WAV
slice/concat/compress), plus a transcribe hold/release so a test can hold a
transcription open and assert what happens meanwhile. Fakes at the device
boundary, which is the only place we put them.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
The Models screen and the Download Manager only knew about whisper. Anything else
that transcribes - pro's Parakeet - had its own private download path, so cancel,
retry and delete were routed to whisper instead, and its progress never appeared
in the shared UI.

sttModelRegistry is the seam: a model registers itself with an id, a size, a
download and a delete, and the app-wide surfaces treat it like any other. Pro
registers itself at activation, exactly as ttsProvider already does, so CORE
STILL NEVER IMPORTS PRO. Adding a third engine needs no change here.

sttProvider routes by owner rather than assuming whisper, which is what fixes
cancel/retry/delete acting on the wrong model. TranscriptionModelsTab renders
registered models alongside the whisper sizes.

The new sttModelPrompt slot lets a service raise the download consent sheet from
the app root. appRoot already holds the TTS engine bridge and a slot maps to one
component, hence a second slot rather than overloading it.

Its pro half is already committed there; the submodule pointer must not move
before this is in.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
An unattended summary that quietly falls back to the on-device model is a jetsam
risk on iOS: the recorder's mic assertion keeps the app alive, so a background
generation holds around a gigabyte and the OS kills the process. The manual path
is fine, because the user is watching it.

`remoteOnly` makes that explicit. When set, a remote failure rethrows instead of
retrying locally, and reaching the on-device path at all throws rather than
silently running. The flag threads through every chunk and the combine pass, so a
long transcript cannot fall back partway.

Default is unchanged, so every existing caller behaves exactly as before.

This is the core half of pro's remote-aware auto analysis. Both must be in before
the submodule pointer moves.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
An iOS memory warning evicts the least-recently-used model. If that was whisper
while a file transcription was running, unloadModel cancelled the job to avoid a
use-after-free - whisper.rn reports the cancellation as `Code: -999` - and the
clip surfaced as "Failed to transcribe" through no fault of its own. The model was
loaded FOR that job.

isFileTranscribing lets model residency veto the eviction instead, so the warning
frees something else.

Ships with the rendered test that fails without it.

Co-Authored-By: Dishit Karia <hanmadishit74@gmail.com>
A background consumer of the single whisper context needs to yield to a
foreground one - locket's queue pausing for voice mode. subscribeRealtime is that
signal.

Watching isCurrentlyTranscribing() would be wrong: it is also true while a
background FILE transcription runs, so a listener would pause itself. This tracks
the realtime session specifically (stopFn), which is what makes it safe to react
to.

Mirrors generationService.subscribe and fires immediately with the current value,
so a late subscriber is never out of step. A throwing listener is swallowed -
nothing a listener does may break transcription.

Pairs with pro's foregroundYield, already committed there.

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>
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>
@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 Aug 1, 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: fea30199-4352-4a3f-b057-98e263c33a47

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

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.

@dishit-wednesday
dishit-wednesday changed the base branch from fix/locket-round-3 to feat/locket-pro August 1, 2026 11:29
dishit-wednesday and others added 11 commits August 1, 2026 17:02
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>
dishit-wednesday and others added 2 commits August 3, 2026 18:04
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
@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@dishit-wednesday
dishit-wednesday merged commit 40ab6e0 into feat/locket-pro Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant