Sync feedback: Mobile runtime, clipboard, and model fixes - #628
Open
alichherawalla wants to merge 171 commits into
Open
Sync feedback: Mobile runtime, clipboard, and model fixes#628alichherawalla wants to merge 171 commits into
alichherawalla wants to merge 171 commits into
Conversation
…droid From Android 10 the platform refuses `primaryClip` to an app that is not on screen. The change listener still fires, so the app learned that a copy HAPPENED and never what it was - and the `?: return` on the read dropped it. That is why nothing copied outside Off Grid ever reached a paired device: the transport was fine and there was simply nothing to send. Accessibility supplies the missing half. A service reports text selections and nothing else - its config declares `typeViewTextSelectionChanged` alone, with no window-content access - and `ClipboardSelectionMemory` holds one selection for 30s, consumed by one copy, so a stale selection can never be published as new. A clip this app CAN read still wins, and clears the memory. The service is off until the user turns it on in Settings, and nothing here asks them to.
… a background copy A fact the platform owns, asked rather than remembered. An older native build that cannot answer is treated as capable, not as denied: reading silence as "off" would nag the user to enable something this build cannot even see.
Nine defects as reported, separated from the two that were not defects and from what is confirmed working. Two carry a cause already: the Android clipboard (the platform refuses a background read) and the desktop mDNS bind (one dead interface takes the whole advertisement down). Muse Glimmer is confirmed rather than guessed - `muse-glimmer` appears in zero files of the llama.cpp that llama.rn 0.12.9 bundles, against 35 mentioning `qwen3`.
…g to migrate Checked the history of DEFAULT_RECEIVE_POLICY rather than assuming: `disabledCategories: []` from its first commit. A stored policy with categories off is therefore a choice made on that machine, not an inheritance - and a migration that cleared it would silently re-enable something a user switched off. Recorded as not-fixed with the reason, and with the one question that would turn it into a real defect.
…ty switch finds the model busy The seat one is the sharper of the two: the mesh already reclaims the least attributable seat before evicting a live device, so a reinstall leaving a ghost behind means that policy did not run on this path. A user should never have to ask for a seat their own phone vacated. His requests are recorded separately from his defects, including the hosted-GPU one, which deserves a plain answer rather than an open question - the promise is that data stays on the user's devices.
…t governs All business logic in shared/sync, hosts as consumers that supply facts and decide nothing. Everything that moves is an item on one durable queue, because a device offline means "not yet" and never "lost". And the tell, written down: "X works here and Y does not" means two code paths doing one job. Four of today's defects are exactly that shape, so the first question for each is where the one owner is - not which side to patch.
`constants`, `appStore` and `projectStore` each carried their own default system prompt - three different texts for one idea, all opening with the same sentence. It matters beyond tidiness. `systemPrompt` is a SYNCED model setting, so whichever copy a device happens to hold is the one that travels to its peers. A report of a desktop reply opening with "A helpful AI assistant running locally on your device" can only have come from a phone, because that sentence exists nowhere else - and with three sources there was no single thing to point at.
The Windows preview is fixed and says how. The persona leak is traced to a real route - systemPrompt is a synced setting and that sentence exists only in mobile - but the value has since been overwritten, so it is recorded as not-fixed with the query to run before touching settings next time. The web-search complaint did not reproduce: that chip renders collapsed. Recorded as needing a screenshot rather than left implying a fix.
…patched yet The seat: reclaim is gated to the cap, and `isUnclaimedSeat` only names a seat with NO syncDeviceId - his ghost has one, so it is orphaned rather than unclaimed. Acting on "no live device answers to this seat" from one node would evict a device that is merely offline or paired elsewhere. It needs an identity that survives reinstall, which is a product decision. The busy error: the send is refused after a 15s `waitForIdle`, while this codebase documents a 74s CPU prefill in two places. So a healthy prefill reads as busy, and Pat's slowness is the same prefill from the other side. The right fix waits on progress rather than elapsed time, and wants a device round first.
…igrate The objection to a migration was protecting choices already in the field. There is no field. What has to be right is a FRESH install, and that is proven: the default accepts everything, and the two categories added today inherit it - asserted in receive-category-coverage.test.mjs. Anurag's Mac is a dev profile whose categories were switched off during earlier testing. A fact about that machine, not about the product.
Anurag tested the current build, where sync ships for the first time - so his section has no installed base behind it. Pat and the Muse Glimmer report are about previously shipped releases, and the app DOES have an installed base even though sync does not. The distinction decides what "nothing to migrate" may be used for: receive policies, yes; a paying user's licence seat, no.
One section per fix, each naming the exact action, the expected result, and the old symptom - so a partial fix cannot pass as a whole one. The re-registered-peer case and the dead-interface case are called out because they are the conditions that produced the reports, and neither happens by accident. Also lists what is NOT fixed, so nobody spends time testing for a fix that is deliberately waiting.
My first cut hid a cancelled attempt. The mobile integration journey disproved the premise by passing without it: it cancels, reads "Pairing cancelled", retries and pairs. Retry-after-cancel already works from waiting_for_confirmation, and the confirmation is wanted. Kept the fold-by-id, which is a real fix for a second route to the same stuck sheet. The untested difference is order: Anurag cancelled an attempt that had already FAILED, and a terminal attempt may have nothing left to cancel. The plan now asks for that sequence instead of claiming a fix.
Pinned exactly rather than caretted: a caret on a prerelease resolves unpredictably, and this is a release candidate whose own CI badge reads failing upstream - so the version we ship should be the version we chose. What it buys: the bundled llama.cpp knows `nemotron`, `nemotron_h` and `nemotron_h_moe`, so Nemotron 3.5 loads. What it does NOT buy: `muse-glimmer` appears in zero files of its cpp/, against 48 mentioning qwen3 - so Muse Glimmer still cannot load on mobile and no setting will change that. The Hexagon kernels already in this repo are byte-identical to the ones this version ships, so the assets need no change.
It implemented the OLD positional API and asserted guide tokens were forwarded - behaviour 0.13 removed. A boundary double that lags the runtime proves the engine against an API that no longer exists. Now: one options object, and the assertion is that we send NO guide tokens, which is the actual guarantee.
…ot the phone 30B, Apache 2.0, multimodal with a dedicated perception encoder. Over 55 GB at full precision, under 20 GB at 4-bit, needing a 24-32 GB envelope on "Mac or PC with a single consumer GPU". That reframes the report: no phone has that envelope, so the mobile attempt could not have succeeded whatever the runtime. The llama.rn gap is real and secondary. Desktop is where it is viable, and b10369 already knows the architecture.
…elease Mobile is on 0.13.0-rc.0, which gets Nemotron 3.5 and not Muse Glimmer. PR #379 upstream syncs to b10362 and names Muse Glimmer, but it is open - and a merge alone is not enough, because the prebuilt native artefacts come from the matching release. So: check again immediately before the release, bump if it landed, otherwise next release. Desktop has it today via b10369.
…rom the plan The clipboard fix is the headline item from Anurag's list and had no section at all - including the Accessibility grant, which is a permission nobody has granted before and the only new user-facing flow in this branch. The llama.rn 0.13 bump had none either, and it is the riskiest change here: a release candidate that moved the TTS API, so OuteTTS speech is the first thing to listen to. Also corrects the branch name in the header.
…rt the app
The app died three seconds after every launch, on every launch. `RNFS.stat` builds its result
dictionary and inserts the file type UNGUARDED:
@{ @"ctime": …, @"mtime": …, @"size": …, @"type": [attrs objectForKey:NSURLFileResourceTypeKey] }
iOS omits a key it cannot determine rather than failing, so `type` arrives nil and NSDictionary raises
`NSInvalidArgumentException: attempt to insert nil object from objects[3]`. The size directly above it
has a nil guard. The startup model scan stats every stored path, and an absolute container path goes
stale on reinstall - so a scan of the user's own models killed the process.
A JS try/catch cannot save this. The exception is raised on the module's own queue and rethrown
natively, so the process is gone before any promise settles, which is why `RNFS.stat(p).catch(…)` reads
as safe in 22 places and is safe in none.
`readDir` answers the same question and cannot fail that way: it guards nil attributes, defaults a
missing size, and derives the type from booleans. This asks the PARENT for the entry instead of asking
the path about itself, and returns null for a missing file - the ANSWER, not an error. Bytes come
through the existing size rule rather than a second conversion.
…t there A plain text message failed with "File does not exist or cannot be opened" because PHOTOS from earlier turns pointed into app containers that no longer exist. The runtime refuses the whole turn over one bad media path, and the model reads images from the entire conversation, not just the message sent. The existence check lived in `completion` - one caller of three. The tool path and the capped-token path converted messages directly, so stale attachments reached llama.rn there. A guard a caller has to remember is a guard the next caller forgets, so it now lives in the one conversion from our messages to model input, and no path can skip it. `isModelVisibleImage` is the single rule for what the model may see: not pending, and has a URI. Six call sites each filtered `type === 'image'` by hand, and a rule about model input has to hold at all of them or it holds nowhere. An announced-but-unarrived attachment has an empty URI, so without this the loader row became a media path pointing at nothing. A file still arriving is also no longer logged as "file gone" - saying a transfer in flight was deleted sends the next reader looking for something that never happened.
A synced file is announced before it is sent, and the gap showed nothing at all - so a generated image on its way from another device was indistinguishable from one that was never coming, and the only way to learn which was to restart the app. The row renders the three-dot loader and the real file name from the announcement. Checked BEFORE every other branch, because a pending attachment has no local file and each branch below reads `uri`. The loader is the shared one, imported directly rather than through the barrel.
The animation had two homes: inside ThinkingIndicator, and a platform ActivityIndicator inside Button. A ring spinner on a button reads as a retry glyph rather than work in progress, so pairing a device and sharing a file both looked like they had failed the moment they started. Every busy state now renders the one component, and a button does not change height when it flips to loading.
`RNFS.stat` and `readDir` report a size as a NUMBER on one platform and a STRING on the other. Ten call sites had each written their own ternary for that - ten chances to get a byte count wrong in a place the user reads it: a size, a free-space check, a "does this file match its manifest" guard.
…eeds it A vision model transferred from another device can arrive carrying its vision tag and no projector: it advertises sight it does not have, the composer refuses the photo, and nothing on screen explains why. The only repair lived in a screen the user had no reason to open, and it answered with a raw 401. The chat now offers the repair where the refusal happens, and both surfaces read one message rule so they cannot describe the same model differently. A model with no upstream says so - an imported model has no repo to fetch from, and saying that is correct. Several matching repos refuse rather than guess: a projector from a different quantisation loads and then reads images wrongly. A repo path is now read from where the model came from rather than rebuilt out of its display id.
…t they are Turning a download into a row was decided inside the hook, mixed with the orchestration around it, so the rule could not be read or exercised on its own.
The Vision badge came from a stored flag while the composer refused images - the two disagreed about the same model, which is what made a projector problem look like a chat bug. Sizes and busy states across these surfaces now read through the shared rules rather than being re-derived per screen.
The list, the preview and the project section each decided what a document was called and whether it was available, so the same file could read differently depending on where it was opened.
A message needs the id its peers know to survive a reload and to be matched against what a peer is already rendering.
…rately not wired Names each fix, what to look for, and what the failure looked like before, so a partial fix cannot pass as a whole one - plus the parts that are recorded but not yet acted on, so testing for them is not wasted effort.
The rules were spread across three files: a pure filter in the message builder, an existence check private to the service, and the same `type === 'image'` test written by hand at each call site. They are one rule read at two depths - what the attachment declares, and what the filesystem can back - so they live together now, and no caller restates either half. Also removes what the safe file reader made dead: size imports and local ternaries that no longer had a caller, which the lint gate caught. The pending row becomes its own component; every other branch in that map reads `uri` and this is the one case with none.
The download screen said "Available Memory". Available is whatever the rest of the phone is doing at that instant, so the same device reported a different number on every launch - and it cannot be compared against a model's size, which is the only reason the figure is on that screen at all. Someone sizing a download against "5.2 GB available" is reading a number that will not exist by the time the model loads. Total memory is a property of the device. "12 GB" means the same thing tomorrow, and it is the number a person already knows their phone by.
…a, a card Answers the three questions the Qwythos failure raised, with a worked reference rather than another guess. One owner: memoryBudget.ts already claims that role for the BUDGET, and the estimate needs the same, because modelPreloader and activeModelService currently answer 8448 MB and 12387 MB for the same model. A real formula: PocketPal's memoryEstimator.ts computes (weights + KV + compute) * 1.1, where KV is derived from GGUF metadata and the actual context length, with exact per-type KV cache byte sizes, sliding-window attention handled, mmproj added separately, and a size * 1.2 fallback only when metadata is missing. Their budget is calibrated from the largest model that has actually loaded on the device - a measurement we already take, in [WIRE-RAM] footprintBytes, and then throw away. And a card: MtpAdviceCard is the existing in-chat shape for exactly this - a title, one action, dismissible. A refusal should name the numbers and the way out, because silence is the defect: the only trace today was fits=false in a debug log while the chat invited the user to start typing.
…ether A holistic view of what the 28 memory integration tests actually pin, and what nothing pins - by model type, by co-residency pair, by eviction trigger, by policy mode, by what the user is told, and by axis. Written after I claimed this subsystem had almost no integration coverage and was wrong: there are 28 tests, and they assert on rendered surfaces rather than importing the residency manager, which is why a grep for the manager missed them. The matrix exists so that mistake is not repeatable - coverage is now something you can look at rather than something someone remembers. The shortlist at the end is what is genuinely uncovered and needs no new feature: context length deciding the fit (the Qwythos case, and no test anywhere mentions context length), the text advisory-vs-authoritative estimate divergence that the image path already has a test for, the embedding model as a resident, the classifier swap, not evicting a model mid-generation, and residency after relaunch. Also records what cannot be tested yet because the feature does not exist - a refusal that names its numbers, and any signal from a silent preload failure.
Nothing stays resident across an app relaunch, so "no test covers residency after restart" is not missing coverage - there is no state to assert. Recorded as n/a rather than left in the shortlist, so nobody spends a day writing a test for a behaviour that does not exist.
…p button Turns on whisper.rn's VAD (`useVad`) so only slices containing speech are decoded, and adds the endpointing it does NOT do: whisper's VAD gates what gets transcribed, it never ends the session, so until now every voice turn needed a manual stop. The end-of-speech signal is the transcript settling, not raw volume - it is already downstream of the VAD and of decoding, so a cough or a passing car moves a meter but does not add words. Two windows: 1.8s once something has been said, 8s when nothing has, since someone who taps record and then thinks has not finished, they have not started. The decision is pure and lives in speechEndpoint.ts; whisperService owns only the timer. Stays on whisper.rn 0.5.5 deliberately. 0.7.x REMOVES transcribeRealtime - the library no longer owns the microphone - so upgrading would mean rewriting the capture pipeline across ~20 files. 0.5.5 already ships the full Silero VAD API for when we want true endpointing.
Follows the label change on ModelDownloadScreen. An "available" figure moves with whatever else the phone is doing, so the same device read differently minute to minute during onboarding. Full suite: 621 suites, 8447 passing, 0 failing.
… on a real phone Three journeys that answer what the jest suite cannot: on a device, which models are actually in memory, what they really cost, and what happens when the next one needs room. The spoken one is the piece that was previously called impossible. A phone's microphone is hardware - WDA taps and types, devicectl copies files, neither injects audio - so the Mac SAYS the words out loud and the phone hears them. Confirmed end to end: spoken "draw a simple green square robot" -> whisper -> image intent -> prompt enhancement -> a rendered picture, synced to macOS, Windows and Android. All three verified green on the iPhone, and they immediately found something: in memory: image + voice + speech (3.6 + 0.3 + 0.1 GB) never resident at any stage: text (Qwythos-9B, selected) The model the user chose never loads, through a typed turn, a spoken turn and an image request, while the app still replies and still draws. Logged to GAPS_BACKLOG. Residency is read from the `-ram` rows, not the model rows: all four rows always render, so reading those would report every device as fully loaded. Two device facts that cost real time are written down in MODEL_RESIDENCY_JOURNEYS.md - the composed row label starts with an icon-font glyph rather than a comma, and tapWhenReady does not open this sheet on iOS.
A journey that inherits whatever model was last selected is not the same test twice: a 0.8B and a 2B answer differently, and a remote gateway model does not exercise on-device vision at all.
whisperService was 527 lines and over the limit. Ending a turn is about time; the service is about transcription - so SpeechEndpointTimer owns the state and the one timer that acts on it. Keeping both in one object also removes the obvious bug in the split version: a stale timer firing against fresh state. 42 suites / 290 whisper-and-voice tests pass.
…irst partial
Auto-stop never fired on device. With `useVad` on, whisper emits NO realtime events until
its own detector trips, so hanging the endpoint off the first partial meant nothing was
ever armed and the turn recorded until it was stopped by hand.
Arms the window when transcription starts instead. Confirmed live on the iPhone:
speaking, then NOT tapping stop...
auto-stopped on silence after ~1.9s of quiet
SPOKE "draw a small orange fish" (turn ended by: silence)
RESULT image
The journey grew a --auto-stop mode that presses nothing and waits the turn out, so this
cannot regress silently: if the button still reads "tap to stop" after 20s it says
"auto-stop did not fire" and taps stop rather than reporting a pass. That mode is what
caught this bug.
…tling The previous endpoint watched the transcript stop changing. That does not work. Whisper's realtime mode re-decodes the whole accumulated buffer every 3s slice, so the text keeps shifting while the room is quiet - punctuation moves, a word gets corrected - and "the transcript settled" never becomes true. It passed on a short clean phrase in a quiet room and failed on real speech, which is the worst way for a check to be wrong. whisper.rn's own `useVad` cannot do this either: in rn-whisper.cpp `vad_simple` only gates whether a slice is DECODED, and `vad()` returns true outright while transcription runs. Nothing native stops the recorder, so there is no parameter that gives auto-stop. Listens to the microphone instead. audioRecorderService already records alongside the realtime transcription, so it now exposes an RMS per buffer, and the endpoint ends the turn after 1.5s of quiet once speech has been heard (8s if nothing was said at all - someone who taps record and thinks has not finished, they have not started). The noise floor adapts rather than being a fixed threshold: a desk in a quiet room and a cafe have very different floors. It falls fast and rises slowly so a long sentence cannot drag it up into counting as silence. NOT yet verified on a device.
This is why auto-stop never ended a turn on the device, in either version. The endpoint called whisperService.stopTranscription() directly. The stop BUTTON does not do that - it goes through useWhisperTranscription.stopRecording, which sets isRecording false, captures trailing audio, stops the service, and finalises the turn. Calling only the service stopped the native job while the UI stayed in the recording state and the transcript was never submitted, so from the outside nothing happened at all. The service now takes an onSilence handler and the hook passes its OWN stopRecording. The service keeps its internal stop as the fallback for callers that supply nothing. Adds [VAD] logging: begin, whether the recorder delivers audio buffers at all, a throttled rms/floor/speech trace, and the end decision - so the next failure names itself instead of being three guesses.
…ally takes Every previous attempt instrumented startRealtimeTranscription. Voice mode never calls it. The device log said so plainly: recording=true in [VoiceButton-SM] while the whole turn produced zero [VAD] and zero WhisperService lines. useVoiceInput records to a FILE through audioRecorderService and transcribes it afterwards with transcribeFile. The realtime API is a separate path used elsewhere. So the endpoint now lives in Voice.ts, starts with the recorder on both recorder-backed paths, and on silence calls stopRef.current() - the SAME stop the button runs, so the turn finalises identically. Also stops listening on stop and on cancel, so a timer cannot outlive its turn. Found by pulling Documents/offgrid-debug.log off the device with devicectl. Worth writing down: that log is in UTC, and reading it as local time made a live log look hours stale.
…ton actually takes" This reverts commit 4f6ccd1.
…ufferLengthInSamples) The library reads options.bufferLength and passes it to native. The wrong key sent undefined, which is a likely cause of the crash seen while recording. Kept because the option name is wrong either way; the feature that used it is reverted separately.
Re-wires the endpoint into useVoiceInput - the path the record button actually takes - but with AUTO_STOP_ON_SILENCE off, so it only listens and logs. Proves the audio side on a device before anything is allowed to end a turn, which is the step that was skipped last time and cost a crash. VOICE MODE ONLY, gated on isInAudioInterfaceMode. Chat-mode dictation is someone typing with their voice: they pause to think mid-sentence and expect the recorder to wait. When enabled, the stop is deferred off the audio callback with setTimeout - stopping the recorder from inside its own buffer callback tears down native state the callback is still standing on, which is the other crash candidate.
Enabled after proving the audio side first, which is the step that was skipped before and cost a crash. Observed on the iPhone with auto-stop off, then switched on: rms=0.1608 floor=0.0081 speech=true speech rms=0.0086 floor=0.0085 speech=false silence [VAD] ENDING turn - heardSpeech=true fired ~1.5s later Driven end to end, touching nothing after speaking: recording started true -> AUTO-STOPPED after 2.0s -> app still alive. The floor now tracks the ROOM, not the speaker: it follows quiet down but speech can never drag it up. On device a sustained sentence had pushed it 0.012 -> 0.052, and a long enough one would have raised the bar above the speaker's own voice and ended the turn mid-sentence. With the fix it held at 0.0081 through 0.16 peaks. Voice mode only. Chat-mode dictation is someone typing with their voice - they pause to think and expect the recorder to wait.
"Stop on silence" - on by default, in BOTH places asked for and verified on the device:
Settings > Model Settings > Transcription vad-auto-stop-{on,off}-button
in-chat settings > SPEECH TO TEXT vad-auto-stop-{on,off}-button
Read at the START of each turn rather than at mount, so toggling it takes effect on the
very next turn instead of needing a reload.
It sits with STT, not TTS, because it is about listening: the microphone decides the turn
is over. Still voice mode only - chat dictation never auto-stops.
On by default because a turn that waits for a tap is the thing we were asked to fix; off is
for anyone who pauses mid-thought and wants the recorder to keep waiting.
49 suites / 556 tests pass.
… its setting Two violations found by review, one of them a live bug. SSOT: whisperService STILL began the endpoint and defaulted onSilence to its own stop, so the realtime path - which is chat-mode hold-to-talk dictation - would have auto-stopped on silence. That is exactly what must not happen: dictation is someone typing with their voice, and they pause to think. Two objects were deciding "the speaker has finished". useVoiceInput is now the only one, and whisperService is back to transcribing. Cohesion: the toggle lived in textGenAdvancedSections, a text-generation module. Voice input is not text generation. Moved to settings/voiceSections.ts, imported by both surfaces so the control itself is still defined once. whisperService drops to 478 lines. 96 suites / 1078 tests pass.
…nd tool cards An opt-in `hideProse` on ChatMessage. Voice mode passes it; nothing else does, so chat mode is untouched by construction rather than by care - the default is undefined. Exists so tool cards and images stay owned by ONE component. The alternative was reimplementing tool-call rendering inside the audio module, which is how two renderers drift until a tool looks different depending on which mode you are in.
getAppMemoryUsage returned `total - used` as available. That is device-wide free RAM and says
nothing about what iOS will let THIS process have. On a 12GB iPhone it reported ~10.9GB while
the process ceiling was ~6.3GB - and this is the figure the model-load gate reads, so a 6.7GB
model was approved and the OS killed the app:
[MEM-SM] checkMemoryForModel modelMB=6756 kvMB=3851 estMB=10608 availMB=10886 ctx=19456 safe=true
[WIRE-RAM] processAvailableBytes = 6.26 GB
computeAvailableBytes in this same file already gets this right - it prefers the real
per-process headroom - and carries a comment warning against exactly this arithmetic ("the
budget thought ~11GB was free when ~1.3GB was"). It was fixed there and left here, and the gate
reads here.
Android has no per-process cap, which is why the same model loaded there and only iOS died.
For a line you must not lose. The timer-based flush is scheduled work, so a step that blocks the JS thread - or a process the OS is about to kill - never reaches it, and the log stops exactly where the interesting part begins. That is how an 11-second main-thread block left no trace: every marker inside it died in the buffer. Forcing a flush before the blocking call is what finally identified it.
… while it does it Dev-only (__DEV__), and it is how the startup crash was found after hours of guessing. Two facts were missing. WHERE startup stops: a [BOOT] marker before every step, so the last line printed names how far it got. WHETHER the main thread is alive: a memory sample every second, flushed immediately - if the ticks stop while the process lives, the thread is blocked and the last tick says when. That combination pinned it in one run: markers reached "load pro features", the probe ticked at t=0.0s and never again, and the block was 1,752 records being replayed synchronously. Also logs the OS's own memory warnings on the same timeline, so a jetsam is distinguishable from a freeze instead of being inferred.
Two fixes to my own rig, both because it was reporting passes it had not earned. waitForOutcome matched ANY generated image on screen - and a chat keeps every picture it has made, so a second run passed the instant it started, before the app did anything. It now counts matches BEFORE acting and requires an increase. The auto-stop check reported success when the screen merely stopped saying "Recording", which is free if recording never started. It now proves recording began first, then times the silence from there - and says "auto-stop did not fire" rather than passing quietly. That is what caught the VAD bug three times in a row. Adds --auto-stop, which presses nothing and waits the turn out, and ensureChat so a journey starts from a known state instead of wherever the last one left the app.
knip caught both on the pre-push gate. stopStartupMemoryProbe is called only by the probe's own window expiry, and AudioLevelListener is only used by the signature it annotates - neither is part of an API, and an export that nothing imports reads as one.
|
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.



Scope\n\nMobile changes for background clipboard capture, safe filesystem reads, llama.rn 0.13, chat and attachment identity, model vision repair, loader consolidation, and the dated manual and repair plans. The PR also points at the Mobile Pro sync-feedback branch.\n\n## Status\n\nDraft. Published to show the complete current delta against main. This branch is not merge-ready. The filesystem fake migration is incomplete and the current full test gate is red.\n\n## Validation\n\n- TypeScript and lint passed before the latest test migration\n- Current Jest failures are recorded in the repair plan\n- Pre-push hooks were skipped for this publication at the owner's direction
Summary by CodeRabbit
New Features
Bug Fixes