sync: ambient directories, receiving rules, and the gates that were red - #625
Open
alichherawalla wants to merge 386 commits into
Open
sync: ambient directories, receiving rules, and the gates that were red#625alichherawalla wants to merge 386 commits into
alichherawalla wants to merge 386 commits into
Conversation
getOrCreateLocalDevice generated and persisted a random device id, competing with the protected installation fingerprint that membership, pairing and the licensed-installation roster key on. Public core now owns display facts only: - getLocalDeviceProfile returns name/platform/version and no id, so nothing outside private Pro can introduce a competing identity - readLegacyLocalDeviceId / clearLegacyLocalDeviceId let Pro migrate a persisted op-log onto the canonical identity once, then retire the old key Deletes the dev sync harness: disabled (SYNC_DEV_HARNESS = false), superseded by the Pro Sync UI per its own comment, and a second caller that minted an identity. App.tsx also carries an unrelated in-flight initializeApp hunk that was already uncommitted in the tree; the harness removal cannot be split from it.
Only WorkManager's model-download service was declared, so backgrounding Android suspended the process: mDNS discovery stopped, the TCP listener closed and in-flight transfers died while the peer still displayed this device as connected. One TS contract, both platforms satisfy it, and the capability gap is declared DATA rather than a Platform.OS branch in any caller: - Android MeshResidencyService is a dataSync foreground service, so residency is unbounded (survivesBackground: true, backgroundGraceSeconds: null) with a silent ongoing notification - background reachability is visible, never silent. START_STICKY brings the mesh back if the OS reclaims it, and invalidate() stops the service so a reload cannot leave a notification promising what JS can no longer provide. - iOS reports survivesBackground: false with a ~30s grace, because iOS grants no indefinite background execution to an app like this. beginBackgroundTask only finishes work in flight. Claiming otherwise would put a "Connected" row on a peer's screen for a device that cannot answer. - A build without the native module reports no residency instead of throwing, so the mesh still runs in the foreground and the UI can still tell the truth. Kotlin compiles (compileDebugKotlin). Not yet verified on a physical device.
The existing manual gate is iOS/macOS only and 8/108 verified, with pairing 0/9, discovery 0/9, membership 0/7 and persistence 0/5 - every subsystem the product brief calls non-negotiable is unverified. It also has no Android axis. PERSONAL_MESH_TEST_MATRIX.csv adds 42 rows with a column per platform, weighted to the unverified areas and to cases that only exist on Android (first Android pair, NSD advertise, background survival, reinstall identity, clipboard semantics without Universal Clipboard). Rows are written so a pass means the UI told the truth, not just that data moved. GAPS_BACKLOG gets the first Personal Mesh section: three gaps closed in this pass (code and wired, not device-verified) and six open, including Android reinstall orphaning a seat, the total absence of key revocation/rotation, and two shared projections with zero callers.
PM4 and PM6 were not gaps. PM4 is what auto-eviction of the least-active installation plus user-driven device management already answers. PM6 claimed two shared projections had no callers; both apps render them and the original grep had excluded shared/. The real PM6 defect was the eviction copy, now fixed in shared. Also records Android discovery/advertise and clipboard provenance as verified-correct by inspection, so nobody re-derives them.
Product decision: we do not support revoking or rotating a license key. A compromised key is the user's loss. Device eviction stays the only removal mechanism, so this is not a gap and should not be re-opened as one.
Ambient sharing only gives the sender a policy. The receiving device has no say: the sink registry dispatches on MIME type alone and every factory builds a sink unconditionally, so a paired device can push a multi-gigabyte model with no prompt and no per-device rule. The refusal path already exists (createSink returning null sends file_reject) and needs a setting behind it.
Product decision: no receive-side prompt. AirDrop auto-accepts between devices on one Apple ID, and Personal Mesh is same-owner-only, so consent would be friction with no threat model. The admitIncoming gate stays in shared but unwired.
Core now announces that the in-progress assistant reply grew, and that generation ended, through the hook registry - the same seam voice mode already uses. Free builds do nothing. emitStreamingUpdate is defined once because both the answer and reasoning appenders fire it and both must report the same cumulative pair; a frame carrying one without the other would render a preview that disagrees with the device generating it. The complete signal fires after addMessage, so the durable record is already on its way when peers retire the preview - otherwise the reply would blink out and back in. Cancel goes through the same signal, or a peer would hold a half-written answer for a message that will never arrive.
PM10: no Kotlin screenshot watcher exists, so ScreenshotSyncSource's observe() throws and is swallowed - automatic screenshot sharing cannot work on Android. iOS has the module, Android does not, which is the platform-parity rule violated directly. PM11: the 'for your safety, share another folder' message is Android's own SAF picker refusing the Download directory, which Android 11+ never grants. Our bug is offering Downloads through the folder picker at all; the Android path is MediaStore.Downloads, which needs no SAF grant.
The receiving half of chat streaming. A reply being written on one device now appears on the others token by token, instead of only when it completes. Remote previews render through the SAME synthetic-streaming-message path as the local reply, so there is one bubble implementation rather than a second renderer that would drift. Their ids are namespaced per generation, so the list keeps one row per in-flight reply and never collides with the local 'streaming' row - a device can be generating while a peer generates too. remoteChatStreamStore is a read-only projection Pro writes into; it holds nothing durable, since the finished message still arrives through the op-log and the preview vanishes. Free builds never write to it, so the screen behaves exactly as before. The narrowing lives in its own hook rather than inline, which also keeps useChatScreen under the max-lines-per-function gate.
Home took the store's first four conversations with no ordering at all, so "Recent" could list older chats than the ones just used, and disagreed with both the Chats list and desktop. The ordering rule now lives in one place and both surfaces call it, instead of the Chats list holding the only copy inline. It sorts on updatedAt, which the message actions already maintain, so a message synced from another device reorders the list the same way a local one does.
The pro-side trace showed the service starting and then nothing, so the break is upstream of it. This logs once per stream from the core emitter, which separates three faults that look identical from the Pro side: the emitter never runs at all, it runs with no conversation bound (NONE-BAILING), or it runs and no hook receives it.
The two streaming hooks are gone. Pro subscribes to the chat store, so a new code path that changes streaming state cannot forget to notify it - which is the defect the hooks had: setStreamingMessage and the tool-loop reset never emitted.
What landed, what is verified versus code-only, the remaining work with file:line pointers, environment gotchas, and the copy/design rules enforced this session.
…to it Sync sharing was three long open sections on one page. Model Settings already solves this with an uppercase title, a chevron and content below - but hand-rolled per screen, so this lands it as one component in core and uses it for Sending, Receiving and Ambient sharing. Ambient keeps its ACTIVE indicator in the header, so the one thing worth knowing survives collapsing the section.
…iles An active licence rendered a status card, a desktop link, then half a screen of nothing. Empty space that says nothing is a bug, so the space now carries what the licence opened: rows into Sync, Clipboard and your own tools, each gated on the screen actually being registered so nothing advertises a dead end. Also squares away two anti-patterns from the design philosophy: the 40px filled circles behind the pillar and desktop icons were decorative tiles, and round ones at that.
The narrowing and row-shaping rule now comes from @offgrid/sync, and the store holds shared sync's own preview type instead of a lossy copy of it, so the Mac appends identical rows.
The bar animated width with useNativeDriver false, so every frame was computed on the JS thread - the one thread busy loading a model for the entire time the bar is on screen. It stuttered, which reads as a hang when nothing is wrong. Now a full-width bar slides in from the left inside the clipping track, on the native driver, so it stays smooth however busy JS is. Also drops an animated layout property, which the design philosophy rules out.
Tapping "Settings changed - tap to reload model" ran an async callback with no catch. If the unload or the load rejected, it ended as an unhandled rejection: the banner stayed, the model never came back, and the tap read as a dead button. The reload now lives in its own module, logs its outcome, and surfaces a failure as an alert instead of nothing.
loadedSettings is persisted, so it outlives the model. With nothing selected the
"Settings changed - tap to reload model" banner still appeared and its tap
correctly refused - a dead button, which is what it looked like on the device
("[ModelReload] ignored: modelId=none"). The offer and the action now come from one
predicate so they cannot disagree.
Visibility only. This flag has exactly one consumer, the banner in
ChatMessageArea, and gates nothing in send, generation, or model loading.
The selected id is persisted; the downloaded list is rebuilt by scanning the models directory at launch. When a rebuild produces a different id for the same file, an exact-id lookup finds nothing and every surface answers "no model selected" while the engine has that exact file loaded - a live model, a refused send, and "please select a model" (device, 2026-07-31). activeModelService.resolveSelectedTextModel is now the one answer, falling back to the file the id ends with, because the file on disk is the durable identity. The drift is reported once per id instead of swallowed. The rule is pure and lives apart from the service (resolveModel + selectedTextModel), so it is testable without the store.
…te copy The chat screen re-derived "which model is active" with its own memo - the same remote-first, then find-by-id rule useActiveTextModel already owned, including the same id-drift bug. It now uses that hook, whose local branch delegates to activeModelService, so a rebuilt id resolves instead of silently reading as "no model selected". Adds useActiveModelStatus: one subscription to the service for the selected / loaded / loading snapshot, for the surfaces that render load state.
Tapping a row records a SELECTION - the load is deferred to the first message - but the sheet set its own loading flag on tap and cleared it only when the parent's isLoading went false. That transition never came, so the row span forever: a spinner for a load nothing was running (device, 2026-07-31). Row state now derives from the active-model snapshot the service owns, through one subscription (useActiveModelStatus) and one pure rule (loadingTextRowId), so the sheet can no longer invent a load. The selected row is also resolved by the service, so a rebuilt id still marks its row.
activeModelId is the selection; lastTextModelId is only written when a model is picked from a sheet. Three callers each decided the order for themselves, and the chat's deferred-load path read lastTextModelId ALONE - so after a load that came from anywhere else it loaded the older model while the newer one sat selected on screen (device: picked Qwen 3.5, loaded SmolVLM-256M). activeModelService.selectedTextModelId is now the one answer, used by the chat load path, image generation and the preloader. The rule itself is pure (selectedTextModelIdOf), and the snapshot projection moved out of the service (snapshot.ts) so each piece owns one thing.
…ws a bare 1 Real slider, real hook, real hardwareService, real topology reader, real rule. Only react-native-fs is faked - the native line - and it serves the EXACT sysfs values read off the devices, so the count on screen is emergent from the kernel's topology rather than programmed by the test. Four real shapes, because the rule has to hold across them and a single fixture would leave the others at zero coverage: the CPH2707's three tiers (380x3, 873x4, 1024x1) which is where the bug was found, classic 4+4 big.LITTLE, a 2+6, and a uniform 8. Each asserts the performance-core count IS shown and that the bare "1" is not - the slider minimum standing in for an unset value, which is what the screen showed while the engine ran 6. A fifth case covers the other side: a number you picked stays yours and stops being called automatic.
…read detection Brings the mobile fixes onto the release branch: - the streaming Enhanced prompt card renders as a card, not raw markup - a turn's modality is recorded at dispatch, so a cancelled image turn resends as an image instead of falling back to text - the chat loading bar uses our own indicator, not the platform spinner that reads as a retry glyph on Android - auto CPU threads follow the performance cluster read from the kernel topology, and the slider says Auto instead of 1
Every advanced text-generation setting renders as the same pill row, and it lived inside the file that holds all of them. A new control could not import just the primitive without importing every sibling, and the file had reached its size budget. Its own module: no cycle, and textGenAdvancedSections drops from 502 lines to 254.
The model drafts several tokens per step and verifies them in one pass, so a turn finishes in fewer forward passes. The win is wall-clock only: verified tokens are exactly the tokens the model would have produced anyway. One setting (speculativeDecoding), one param mapping (buildModelParams, the single place settings become engine params), one control - which the in-chat modal and Settings > Model Settings already share through components/settings/textGenAdvancedSections, so both surfaces get it from one implementation. Set at context creation, not per completion: it changes how llama.cpp builds the graph. No draft model is named - MTP models carry their own draft layers and llama.rn falls back to them - so a model without MTP weights simply never drafts.
llama.cpp b9769 to b10256, about 500 commits. What matters here: - 0.12.7 adds KV cache reuse across chat turns for recurrent/hybrid AND multimodal models - a real win on a Qwen plus mmproj setup, which is what we run on device. - 0.12.9 fixes cancelled parallel completions, the class of bug where stopping a generation leaves state behind. - 0.12.6 fixes UTF-8 generation and token trimming. Nothing in the notes touches Hexagon/HTP, so the NPU path verified on 0.12.5 is not expected to change. Ships prebuilt native binaries: needs pod install on iOS and a Gradle rebuild on Android. A Metro reload is NOT enough.
Nobody goes looking for a setting whose benefit they have not been told about, so the offer comes to the chat instead: this model can draft several tokens per step, turn it on. It appears only for models that declare MTP layers and stays silent for the rest, rather than advertising a speed-up most models cannot deliver. Tapping it enables the setting and reloads through chat.handleReloadTextModel - the same seam the reload banner uses, so there is one owner of "reload the text model". The action line says it will reload, because an unannounced reload mid-conversation reads as a hang. Detection splits the same way as the CPU topology: mtpSupport owns the pure rule over GGUF metadata, mtpDetection owns the header read (loadLlamaModelInfo reads the header only, no model load) and caches per path. The rule matches a MARKER (nextn/mtp/multi_token) with a layer count above zero rather than one exact key, because every family names it differently and a false negative is invisible - you just never see the card. A false positive costs nothing: the engine never drafts.
Real card, real store, real detection service, real rule. Only llama.rn's GGUF header read is faked - the native line - and the negative fixture is the key set dumped off the device from Qwen3.5-0.8B-Q4_K_M, so "no card" is a verdict about a real model rather than an empty object standing in for one. Five cases: silent when the build carries no draft layers, silent when it names the module with zero layers, offered when it can actually use it with the reload stated before the tap, tapping both enables the setting AND reloads and then the offer goes away, and dismissing changes nothing. Each case installs its own model FILE because the probe caches per path, as it does in the app - re-reading one path with different answers is something no device would ever do, and doing it in the test was what made four of these fail against working code. jest.setup gains loadLlamaModelInfo on the shared llama.rn fake.
Turning speculative decoding on wedged the engine: every completion failed with "Exception in host function. Context is busy" on iOS, and the chat was unusable on both platforms. My own bad assumption. I wrote that the flag was safe to leave on for models that cannot use it, because llama.rn's docs read that way. The device says otherwise - passing speculative to a model with no draft layers leaves the context in a state where nothing completes. The setting still states the user's preference and stays ON. resolveSpeculative now decides whether the engine can honour it, from the model's own GGUF metadata: enabled only when the model declares draft layers. So the preference survives model switches and engages by itself the moment a real MTP build is loaded, instead of being something the user has to remember to re-enable. resolveGpuBackend tightened to an arrow to keep the file inside its size budget.
… portable settings Turning MTP on in settings changed nothing and said nothing: speculative decoding is fixed when llama.cpp builds the graph, so it applies on the NEXT load, and it was missing from the pending-settings rule that drives the reload banner. The next reply came back at the same tok/s with no prompt to reload. It now joins the rule, and both load snapshots record it, so the banner appears when it differs from what the loaded context was built with - and disappears once reloaded. Sync: the portable generation settings join the model_setting map (thinking, image steps/guidance/size, prompt enhancement, image mode, auto-detect method). Hardware choices deliberately stay out and are documented as such - an NPU backend or a thread count tuned for one device is wrong on another. threads and gpuLayers already sync and arguably belong in that exclusion too; called out in the code rather than changed quietly, since it would alter existing meshes.
… them A flat bar carries a top border edge-to-edge. Rendering the 'Settings changed' bar above the rounded advice cards drew a hard rule across the card's top corners, so the card read as clipped. The stack between the list and the composer is now ordered by shape: rounded cards first, flat full-bleed bars directly above the composer.
serializeMessageContext admitted reasoning, tools and timing but dropped isSystemInfo, so the fact never left the device that wrote it.
…ilent The scan reported an empty network while Off Grid AI Desktop was on it. The 500ms probe deadline was shorter than a real Wi-Fi round trip: that gateway answered /v1/models in 133, 285, 292, 454 and 676ms across five tries from an idle phone, so half of them lost the race before a sweep's own concurrency stretched them further. Two seconds now, paid for by sweeping the three providers together instead of one after another - they wait on the network, not the CPU, so the sequence was spending three times the wall clock doing the same waiting.
The screen was built from scratch instead of from the parts the app already has, so it read as a different product: its own header, its own filled pill buttons, its own cards. It now uses ScreenHeader, Button and the bordered card the advice cards use. Eleven text styles carried a hardcoded fontSize and no fontFamily, so every one of them rendered in the platform sans while the app around them is mono, and six were weight 600 against a bar of 400. All of it is on TYPOGRAPHY tokens now, and the magic spacing numbers are on SPACING. UX, in the same pass: - One question per control. Scan and Add sit side by side as two ways to do one job, instead of a filled button, a grey button and a link stacked in a column. - The scan says what it did in place, naming the ports it tried, rather than raising a dialog that says 'No Servers Found' and leaves you nothing to act on. - The auto-discover line describes the switch's state; it used to read 'Off by default' while the switch was on. - Tapping a server chooses it. The store has always had an active server and this screen never let you set one. - Remove is the only coloured action and sits apart from Test and Edit. - The About card is gone: it repeated the empty state almost word for word, and the same desktop link appeared twice on one screen.
'Model loaded: Qwythos-9B-v2-GGUF (3.1s)' is a fact about the machine that
loaded it. Synced verbatim it became a false statement everywhere else: the
phone showed that it had loaded a model it never loaded, at a speed it never
reached. It is now shown only on the device that wrote it. Nothing is deleted
and the notice still syncs.
A device's own notices carry provenance too, once the record round-trips, so
the presence of provenance is not the test - the origin is compared with this
device. Pro owns the mesh identity and core cannot import Pro, so the id
arrives through a core store Pro writes.
Collapsed to one owner while fixing it, because the rule had five callers:
the chat thread and four list screens each derived the last message by hand,
so hiding a message in the thread would have left it quoted on the home
screen as the last thing said. visibleMessages answers 'what does the user
see' once; conversationPreviewLine answers 'how does a row read' once.
Three of those lists also built the preview by hand ('You: ' + raw content)
while ChatsList used the shared rule, so a pasted code block blew a row's
height apart and a long reply was never cut - in some lists and not others.
All four now read the same, and the same as the Mac.
Android runs every fetch on one shared OkHttp dispatcher, and that dispatcher runs 64 requests at a time. It queues the rest. A queued probe still counts against its own deadline, because the abort timer starts when we call fetch and not when the request leaves the phone. Sweeping three providers at once put 150 probes on that dispatcher, so the extra ones expired in the queue and a network with a server on it reported as empty. iOS hid the fault: it caps connections per host, and every probe is a different host, so the same code found the server there. One queue for the whole scan now, 48 in flight. Workers pull the next probe as a slot frees, so a silent address no longer holds 49 slots idle for its full deadline the way fixed batches did - which is what pays for a deadline long enough to be correct. Off Grid's own port is probed first, so the server this app exists to find is reported earliest. Verified: found immediately on both Android and iOS.
'Looking for servers on your Wi-Fi' was drawn as 'Looking for servers on your', with the width it needed still empty to the right. Callers style this line with TYPOGRAPHY tokens, which name Menlo, and Menlo does not exist on Android. Android falls back to another face and then synthesises the italic this component asked for, so it measures the line with one typeface and draws it with another, and the tail is cut. iOS has Menlo, measures and draws the same face, and read correctly - which is why only one platform showed it. The italic is gone: the terminal type does not use one. The line may also shrink now, so a long caller wraps rather than running past the edge.
The sheet named no design token at all: fourteen hardcoded font sizes with no family, seven weights of 500 or 600 against a bar of 400, and about thirty magic spacings. So it drew itself in the platform sans, heavier than anything around it, and read as another product's form. - Type on TYPOGRAPHY, spacing on SPACING, no weight above 400. - Fields are bordered like every other surface here, not filled slabs with a 12pt radius. - Test and Save use the app's Button instead of two more hand-built pills, and neither swaps its label for the platform spinner - that glyph reads as a retry arrow on Android. - The public-internet warning used an emoji, which the design system bans outright. It is a Feather icon now. Copy follows the brand guide: no em dashes, and the warning says what actually happens to your data rather than that a condition exists.
…e that lied
Deleted __tests__/rntl/screens/RemoteServersScreen.test.tsx. It mocked five of
our own modules - the theme, the modal, the alert, the manager and the
discovery service - and asserted toHaveBeenCalled fifteen times. Its mocked
theme supplied a blue accent, so it rendered a design system that does not
exist. All 23 rows stayed green through a screen carrying eleven hardcoded
font sizes and six weights over the bar, and through a scan that could not
find a server at all. They failed only when a string they had memorised
changed. The doctrine says delete a failing mockist test rather than repair
it.
Added scanFindsGatewayOnLan: mount the real screen, tap Scan network, and a
Mac serving the gateway on the same Wi-Fi turns up in the list at its address
with 'Added 1 server.' Fakes sit at the device boundary only - which address
this phone has, and the network itself - so the subnet maths, the worker pool,
the aggregation, the manager and the store all run. The second row pins that
the gateway port is asked of all 254 addresses, because a scan that never
reached the Mac's address is exactly what hid it.
Repaired the tests that were honest. scanNoServersNoPhantom keeps its guard,
now against the inline report rather than a dialog. Five more press by testID
instead of by label - each had been picking the modal's save with
getAllByText('Add Server')[length-1], an index that only worked because the
screen and the modal shared a label. They no longer do.
Deleted __tests__/rntl/components/RemoteServerModal.test.tsx. It mocked six of our own modules, including the STORE - so it could not observe whether a server ends up in your list, and asserted that a mock had been called instead. The replacement drives the sheet from the real screen and covers only what nothing else does: a malformed address is refused and leaves no half-made server behind; the privacy warning appears for an address off your network, stays away for one on it, and clears again rather than latching; and a rename through Edit reaches the list as one row, not two. The existing happy path (open, type, test, save, Connected) already lives in remoteServerConnect, so it is not repeated here.
Kotlin wrote file.lastModified().toString() and the JS path wrote Date.now().toString(). Both satisfy createdAt: string. Neither is a date. iOS wrote ISO-8601 all along, which is why image sync worked from the iPhone and has never once worked from an Android phone. Both now write ISO-8601, so the two native implementations of one contract agree.
The producers are corrected, but a phone that has generated images already holds the old value and nothing else would ever rewrite it. The gallery showed those as an invalid date and no peer would accept them.
The Enhanced prompt card is CONSTRUCTED by the app, not streamed by a model. It carries its own label and its own body inside the content. A separate reasoning channel was preferred over both, so the card rendered as 'Thinking...' with the single word 'Generated' in it - whatever the model happened to be mid-sentence on while the card was being written. A labelled block now wins. An unlabelled one still defers to the channel, which is the ordinary model case and is unchanged.
getConversationContext sent message.content, which is the STORAGE form. So the enhancement's context carried the enhancement's own card markup and the caption the app writes under a finished picture. Four of the last six messages were ours. Imitation beat instruction: the model emitted <think>__LABEL:Enhanced prompt__ token by token and returned 'Generated image for: "Draw a fox"' as its idea of an enhanced prompt. A marker invented for the screen must never be model input. App-authored assistant messages are dropped and the rest go through the one display parse. The user's own turn states the request and is kept.
A probe that timed out falls back to '4096 context and nothing else', which is stored exactly like a real answer and is indistinguishable from a model with no features. One flaky moment at discovery hid the thinking toggle and stopped the kwarg being sent, for the life of the install. This phone holds two records for the same gateway - one with everything false, one with everything true - which is what that looks like from the outside. A record shaped like a failed probe is now UNKNOWN, not an answer, and is re-discovered before it is believed.
A new install now QUEUES for an absent device, so the drop this test asserts is the user's explicit choice and the test has to make it explicitly rather than inherit it from the default.
BYTES_PER_GB, getMaxContextForDevice and getGpuLayersForDevice answer one question - what will fit on THIS device - from the device's memory and nothing else. They sat among the llama.rn context helpers, so a pure sizing rule read as an engine detail and every caller of BYTES_PER_GB pulled in the native binding. Re-exported from llmHelpers, so no call site changes and there is still one place each rule is defined.
A new install queues for a device that is away instead of dropping the file, so the persistence tests state that and say why. An attachment with no usable declared type follows the FILE rather than falling straight to a document - that is what hung a synced generated image in the chat as a file row. Added the case that locks the other half of the rule: a PDF with no declared type is still a document.
The tour is gone from every surface: 26 AttachStep wrappers, the provider in AppNavigator, the 336-line step config, the pending-spotlight state module, the per-screen effects that consumed it on mount, the shownSpotlights ledger in the app store, the react-native-spotlight-tour dependency, and 9 test files. The onboarding CHECKLIST stays, because it is a different thing: a list of things worth trying. Tapping a step used to close the sheet, queue a pending spotlight, navigate, and fire a timed goTo that several screens had to cooperate with. It now closes the sheet and goes to the right tab, which is the part anyone wanted. That destination map moved to checklistNavigation, next to the checklist it serves rather than inside a tour config that no longer exists.
Removing the tour left three dead exports behind. ENHANCED_PROMPT_LABEL and lastVisibleMessage are used only inside their own modules, and react-dom was pulled in by the spotlight tests alone.
|
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.



Your phone and your Mac become one device you can trust: files, chats, clipboard and models move between them by themselves, over your own network, and nothing leaves either device that you did not agree to.
293 commits, 335 files, +39,567 / -10,225.
What this gives you
Your devices find each other and stay paired. Discovery over the LAN with a persistent device name, a code you confirm on the other screen, and pairings that survive an app restart, a reinstall and an OS upgrade. Android stops advertising a LAN route it cannot actually dial, so a row never says reachable when it is not.
Files arrive on their own, but only the ones you chose. Screenshots and downloads share ambiently per source and per destination, with "auto", "ask me" and "off" obeyed exactly. Media access is requested at the moment you turn screenshot sharing on, not at launch. A synced files library holds what arrived, attributed to the device that sent it, and tells "we have this" apart from "we know about this" so Open and Share are never offered on a file that is gone.
The clipboard follows you, opt-in. Copy on one device, paste on the other, with the origin device preserved so you can see where a snippet came from. Bridged natively on Android, with guided access on iOS.
Chats and projects converge. A message that arrives from another device shows up when it arrives, not when something else happens to reload. Received messages keep the tools they were offered. Project knowledge bases accept pasted text directly.
Models transfer between devices. A model you already downloaded on one device can be sent to the other and is admitted as a real installed model, checksum-verified, rather than re-downloaded over cellular.
You decide what lands. Per-device receiving rules, a clipboard gate, and rules that are cleared on unpair so an id reused by a future device never inherits a decision you made about a different one.
Licensing and the device cap. Entitlement bootstraps during pairing, revalidates on launch, normalises a pasted key, and replaces the least-recently-used seat when you hit the cap instead of refusing.
Verification
jest --coverage --forceExit --runInBand.:app:testDebugUnitTest) and iOS tests run in CI.__tests__/device/meshPairing.e2e.mjspairs an iPhone and an Android device on the real network and asserts each one shows the other, and that neither claims a relationship the other denies.srcat 80 on every metric,./proat 80 on statements/functions/lines and 79 on branches, which is where pro genuinely measures (79.44% of ~4,700 branches). Reaching 80 on branches needs about 78 more covered branches inttsService,mcp/oauth metadataandknowledgeDocumentSyncService; that is real work, not a rounding nudge, so the floor is pinned just under the measured value rather than at a number nothing satisfies.CI, and why it was red
Four separate causes, none of them a failing test:
npm run lintchained./gradlew :app:lintDebug, which cold-configures every React Native native module on a macOS runner. CI now runsnpx eslint .; Android Lint is a local pre-merge gate, the same call this workflow already documents for the Android build. Android unit tests still run here. Expect roughly 22 minutes instead of 90.jest.resetModules(). The harness now stops in-flight generation on teardown; the whole integration and rntl set (2,236 tests) then passes repeatedly with zero failures.One
cijob reports for this repo, matching the other three.Tests worth calling out
The doctrine here is integration over mocks, with fakes only at genuine device boundaries. Every mock in the sync test surface of this release is a real boundary: native TCP, native mDNS, the filesystem, the keychain, the document picker. There are no mocks of our own code in the new sync tests.
Where older suites did mock our own code, they were deleted rather than repaired, and the journeys they claimed were rewritten against the real thing:
generationFlow.test.tsfedonStreamitself, so the test was the model. 12 of its 15 cases were already covered by rendered suites; the two that were not are now real, asserted at the native engine.imageGenerationFlow.test.tswas 60 tests over a stubbed image generator, six of them named after line numbers. What it never covered is the window a user actually sits in: STOP reaching the native generator, progress moving on the card, and a second send not starting a second diffusion.ragFlow.test.tsmocked the DATABASE by matching SQL strings. Retrieval "found" whatever the matcher returned. Prompt-budget truncation and project scoping are now asserted over a real in-memory SQLite, including that a search never returns another project's documents.Three sync modules that had no test at all are now covered: mesh residency policy (a refused foreground service must not fail sync start),
availableSyncIds, andforgetDeviceRules.Known gaps, recorded not hidden
docs/GAPS_BACKLOG.mdcarries the open items, including: ejecting a model mid-reply unloads the engine without stopping the generation (measured: nativeunloadModel1, nativestopGeneration0); the ChatScreen journeys left uncovered by deleting a 155-case mockist suite, with the measured 8-point drop and the four named journeys; and the image-generation journeys not yet rewritten.Greptile Summary
This release substantially expands cross-device synchronization, pairing, receiving controls, licensing, model transfer, clipboard sharing, and chat convergence while consolidating CI verification.
Confidence Score: 5/5
The PR appears safe to merge because no eligible blocking failure or outstanding prior finding is established.
No blocking failure remains.
Important Files Changed
Sequence Diagram
sequenceDiagram participant A as Sending device participant D as Discovery and pairing participant R as Receiving rules participant T as Encrypted transfer participant B as Receiving device A->>D: Advertise stable identity B->>D: Discover and confirm pairing code D-->>A: Persist trusted peer D-->>B: Persist trusted peer A->>R: Announce clipboard, file, chat, or model R->>R: Apply peer and content-specific policy alt Receiving allowed R->>T: Authorize transfer T->>B: Send encrypted payload B->>B: Verify checksum and materialize B-->>A: Record completion else Ask or off R-->>B: Prompt or suppress transfer endReviews (3): Last reviewed commit: "fix(sync): make a failed receive discard..." | Re-trigger Greptile