Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
223 commits
Select commit Hold shift + click to select a range
2ddc40c
feat(clipboard): a copy made in another app still has its text, on An…
alichherawalla Aug 12, 2026
0a7167a
feat(clipboard): the boundary answers whether this device can capture…
alichherawalla Aug 12, 2026
fcee17d
docs(feedback): the cross-device pass of 2026-08-12, and what it found
alichherawalla Aug 12, 2026
f93fcd9
docs(feedback): the receive default was never off, so there is nothin…
alichherawalla Aug 12, 2026
e4a77e3
docs(feedback): Pat's report — a reinstall costs a seat, and a modali…
alichherawalla Aug 12, 2026
9d73f51
docs(feedback): the rule this list is fixed under, at the top where i…
alichherawalla Aug 12, 2026
3693a18
refactor(prompt): one owner for the default persona, not three
alichherawalla Aug 12, 2026
ddb11a0
docs(feedback): what was fixed, and where the evidence ran out
alichherawalla Aug 12, 2026
56bb937
docs(feedback): Pat's two, diagnosed to the line, and why neither is …
alichherawalla Aug 12, 2026
5be0b50
docs(feedback): first release, so the receive policy has nothing to m…
alichherawalla Aug 12, 2026
20d8577
docs(feedback): say which release each report is against
alichherawalla Aug 12, 2026
3aa3b0f
docs(test): what to verify by hand, and what the failure looked like
alichherawalla Aug 12, 2026
6126229
docs: the pairing fix is withdrawn, and the test plan says so
alichherawalla Aug 12, 2026
3985f20
chore(llama): move the runtime to llama.rn 0.13.0-rc.0
alichherawalla Aug 12, 2026
b1d147f
test(tts): the OuteTTS double matches the runtime we ship
alichherawalla Aug 12, 2026
a2fd75b
docs(feedback): Meta's own numbers put Muse Glimmer on the desktop, n…
alichherawalla Aug 12, 2026
a1ed12c
docs(feedback): the llama.rn decision, and what to check before the r…
alichherawalla Aug 12, 2026
4304f39
docs(test): the Android clipboard and the runtime bump were missing f…
alichherawalla Aug 12, 2026
2fe04e3
fix(fs): one safe way to ask about a file, so a stale path cannot abo…
alichherawalla Aug 13, 2026
4f1ea02
fix(llm): every path that builds model input drops images that are no…
alichherawalla Aug 13, 2026
d297b19
feat(chat): a file still arriving shows on its own message bubble
alichherawalla Aug 13, 2026
92be40f
fix(ui): one loader everywhere, and it is never a ring spinner
alichherawalla Aug 13, 2026
3dea1a0
fix(fs): one rule for reading a file size off the filesystem
alichherawalla Aug 13, 2026
f8d6d6d
feat(vision): repair a model's missing projector from the chat that n…
alichherawalla Aug 13, 2026
7563cf9
refactor(downloads): the screen renders rows, the mapping decides wha…
alichherawalla Aug 13, 2026
219f162
fix(models): a row stops advertising sight the model does not have
alichherawalla Aug 13, 2026
c59ed4e
fix(knowledge): a project document reads the same on every surface
alichherawalla Aug 13, 2026
1e2f639
fix(chat): a synced turn keeps its identity in the store
alichherawalla Aug 13, 2026
4457bc4
docs(test): what to verify by hand on this branch, and what is delibe…
alichherawalla Aug 13, 2026
10e357f
refactor(llm): one owner for what the model may be shown
alichherawalla Aug 13, 2026
1de2198
chore(pro): point at the sync feedback release
alichherawalla Aug 13, 2026
88fbcbc
docs(release): track the sync feedback repair plan
alichherawalla Aug 13, 2026
374d698
docs(release): record the draft PR train
alichherawalla Aug 13, 2026
f093c65
test(fs): one native filesystem boundary for every suite
alichherawalla Aug 13, 2026
3379b68
test(fs): model safe file metadata through directory entries
alichherawalla Aug 13, 2026
783dc27
test(audio): use the shared native filesystem boundary
alichherawalla Aug 13, 2026
58fb0e2
test(logs): exercise rotation through safe file metadata
alichherawalla Aug 13, 2026
15435d0
test(sync): render one receive switch per content type
alichherawalla Aug 13, 2026
7e01202
fix(image): exclude runtime notices from prompts
alichherawalla Aug 13, 2026
3ba238d
docs: update sync repair evidence
alichherawalla Aug 13, 2026
b5a05c2
docs: record Windows image-fix parity
alichherawalla Aug 13, 2026
405a366
docs: record image fix publication
alichherawalla Aug 13, 2026
58581c7
fix(llm): keep prompt space in context budget
alichherawalla Aug 13, 2026
c4f4b6c
fix(models): cancel active work before eject
alichherawalla Aug 13, 2026
b4a5123
test(sync): verify active file delivery controls
alichherawalla Aug 13, 2026
95ea85e
docs(sync): record bounded queue verification
alichherawalla Aug 13, 2026
22a4730
docs(sync): record knowledge document join fix
alichherawalla Aug 13, 2026
1beb8a0
test(sync): prove reconnect skips document bytes
alichherawalla Aug 13, 2026
36826ae
docs(sync): record legacy project row migration
alichherawalla Aug 13, 2026
d98292f
test(sync): align mobile queue projections
alichherawalla Aug 13, 2026
3f3779a
docs(sync): record shared queue refactor
alichherawalla Aug 13, 2026
1a793cb
chore(pro): update shared queue adapter
alichherawalla Aug 13, 2026
979cc32
chore(pro): update sync receive identity
alichherawalla Aug 13, 2026
1dc9ceb
docs: record sync release verification
alichherawalla Aug 13, 2026
d9884fd
docs: record hidden live media verification
alichherawalla Aug 13, 2026
e84e6f7
fix(ui): render dropdown options as overlay
alichherawalla Aug 13, 2026
27e52d9
docs(sync): record virtualized mobile lists
alichherawalla Aug 13, 2026
c44d43e
test(sync): cover generated image chat association
alichherawalla Aug 13, 2026
e3f479e
fix(sync): wire mobile mesh healing and live presence
alichherawalla Aug 14, 2026
bc14a27
docs(sync): extend release feedback verification
alichherawalla Aug 14, 2026
5d08292
test(sync): keep rename on this device
alichherawalla Aug 14, 2026
81fbe4a
fix(chat): group synced image results
alichherawalla Aug 14, 2026
788e7da
chore(pro): update shared file recovery
alichherawalla Aug 14, 2026
2527623
fix(sync): align mobile chat image and clipboard state
alichherawalla Aug 14, 2026
1725b49
fix(android): sync copies made outside the app
alichherawalla Aug 14, 2026
9879e6f
test(audio): cover llama.rn 0.13 contract
alichherawalla Aug 14, 2026
7f11b75
refactor(mobile): remove stale public exports
alichherawalla Aug 14, 2026
78e001c
fix(android): capture explicit floating toolbar copies
alichherawalla Aug 14, 2026
48655a1
fix(models): harden file metadata and repair flows
alichherawalla Aug 14, 2026
3664625
fix(sync): align live chat and sharing lifecycle
alichherawalla Aug 14, 2026
48c1381
fix(models): close revision and projector review gaps
alichherawalla Aug 14, 2026
9350ea9
refactor(chat): split screen lifecycle and actions
alichherawalla Aug 14, 2026
6682a96
ci: align pro branch coverage ratchet
alichherawalla Aug 14, 2026
75ec10d
test(e2e): automate generated image mesh journey
alichherawalla Aug 14, 2026
addcc9e
fix(chat): detect LiteRT token-only reloads
alichherawalla Aug 14, 2026
86c20e5
ci: allow ambient sharing journey to finish
alichherawalla Aug 14, 2026
c02f306
test: consume scripted llama completions once
alichherawalla Aug 14, 2026
358a5fd
test: script queued native reply explicitly
alichherawalla Aug 14, 2026
43f520a
feat(settings): one owner for text-generation settings, incl. Maximum…
alichherawalla Aug 15, 2026
2fa3b96
refactor(llm): take the context budget from the user's settings
alichherawalla Aug 15, 2026
4a59123
fix(chat): keep each thinking segment in its own block, and last in t…
alichherawalla Aug 15, 2026
2577ac1
feat(share): let the share prompt be dismissed for good
alichherawalla Aug 15, 2026
2421319
test(e2e): label the screens the attended mesh journey drives
alichherawalla Aug 15, 2026
1a2880c
chore(mobile): import Whisper models from their own module, refresh P…
alichherawalla Aug 15, 2026
601662d
test(e2e): attended Android-to-mesh tool-calling journey
alichherawalla Aug 15, 2026
11872b7
chore(pro): bump submodule for MCP screen test handles
alichherawalla Aug 15, 2026
c7c7af1
test(sync): assert the knowledge toggle announces its state
alichherawalla Aug 15, 2026
3ae4c2f
refactor: split the two files that outgrew the 500-line cap
alichherawalla Aug 15, 2026
1bc3c56
refactor: give each split module its own import site, with no cycles
alichherawalla Aug 15, 2026
3ff4525
refactor: one owner for the recovered-model predicates, and drop a st…
alichherawalla Aug 15, 2026
e598f34
fix(chat): never let a long project name squeeze the model control out
alichherawalla Aug 15, 2026
c2afb66
fix(chat): one owner, one rhythm for every tool row
alichherawalla Aug 15, 2026
5a19f97
refactor(chat): the store owns "a model is loading" and how a reply e…
alichherawalla Aug 15, 2026
b6d6638
fix(chat): show a peer loading its model instead of "Preparing reply..."
alichherawalla Aug 15, 2026
67e3314
fix(projects): stop the last project slipping under the bottom edge
alichherawalla Aug 15, 2026
de3de41
chore(pro): bump submodule for the load and stop sync signals
alichherawalla Aug 15, 2026
fdf0f9d
fix(chat): stop a peer's tool calls appearing twice on the phone
alichherawalla Aug 15, 2026
420f81b
fix(settings): cap Max Tokens by the context that has to hold it
alichherawalla Aug 15, 2026
e7ada17
fix(settings): keep the max-tokens ceiling rule module-private
alichherawalla Aug 15, 2026
ccc5017
test(e2e): put Android in a known image-generation state before a mes…
alichherawalla Aug 15, 2026
425b17d
test(e2e): give the image settings accordion a handle
alichherawalla Aug 15, 2026
a1f59dc
test(e2e): read one attribute of a control from the device
alichherawalla Aug 15, 2026
db945cc
test(e2e): drive image setup through Appium, and prove each value landed
alichherawalla Aug 15, 2026
5dffe12
test(e2e): a vision + generated-image journey, from a real photo
alichherawalla Aug 15, 2026
fed2ef1
test(e2e): let the tools journey be driven from either phone
alichherawalla Aug 15, 2026
a6e9fce
test(e2e): a vision-only journey, and one shared Android producer
alichherawalla Aug 16, 2026
6493c19
fix(chat): the tools-sent row ends where its bubble ends
alichherawalla Aug 16, 2026
fbd45e7
fix(a11y): the quick-settings rows are five controls on iOS, not one
alichherawalla Aug 16, 2026
ec73cb7
test(e2e): the normal-message stage drives whichever device --primary…
alichherawalla Aug 16, 2026
61a3fc8
docs(e2e): a brief for driving the mesh suite from iOS
alichherawalla Aug 16, 2026
5ce1514
test(e2e): a desktop is a list of places to look, not one address
alichherawalla Aug 16, 2026
daf2540
docs: the open concerns, with evidence, for whoever picks this up
alichherawalla Aug 16, 2026
5de106b
fix(e2e): wait for the quick-settings sheet, do not sleep 500ms at it
alichherawalla Aug 16, 2026
50969c7
feat(e2e): start a mesh run from a known state, not from whatever was…
alichherawalla Aug 16, 2026
2435c77
fix(e2e): park the desktops on Day, so re-entering Chat lands on the …
alichherawalla Aug 16, 2026
dcad0f7
fix(e2e): prepare-thinking honours --primary instead of always drivin…
alichherawalla Aug 16, 2026
029d24b
fix(e2e): run-thinking sends from the --primary device, not always An…
alichherawalla Aug 16, 2026
e0c793e
feat(e2e): seed iOS project fixtures with devicectl, unblocking prepa…
alichherawalla Aug 16, 2026
8d61948
fix(a11y): a project row is one control with a name, not a run-on of …
alichherawalla Aug 16, 2026
c05d88e
fix(knowledge): the paste-note sheet is keyboard-aware, so the note c…
alichherawalla Aug 16, 2026
f2689f3
fix(e2e): measure the keyboard before pressing a sheet's action, neve…
alichherawalla Aug 16, 2026
6044120
fix(knowledge): cap the note field with the keyboard up, and press th…
alichherawalla Aug 16, 2026
79d2170
fix(e2e): check the Knowledge Base switch where it lives, under its r…
alichherawalla Aug 16, 2026
1e39c17
fix(e2e/ios): tap the control, not the wrapper that happens to contai…
alichherawalla Aug 16, 2026
5574c3d
fix(a11y): a Knowledge Base row is three controls, and its Back butto…
alichherawalla Aug 16, 2026
6b124ab
fix(a11y): the popover's dismiss scrim merged the whole sheet, too
alichherawalla Aug 16, 2026
cb968bd
fix(chat): a tool row ends where its message bubble ends
alichherawalla Aug 16, 2026
a33c176
fix(e2e): the guided-tools journey runs from iOS, and resumes after a…
alichherawalla Aug 16, 2026
d5551c5
docs(gaps): drop the keyboard entry - it is fixed, not open
alichherawalla Aug 16, 2026
36ed917
docs(gaps): three defects the guided iOS journey surfaced on hardware
alichherawalla Aug 16, 2026
f5105a4
docs(gaps): the final answer is LATE, not lost - correcting my own entry
alichherawalla Aug 16, 2026
a34f0b0
feat(e2e): the generated-image journey runs from either phone, with e…
alichherawalla Aug 16, 2026
fa72c65
fix(e2e): wait for the screen to arrive, do not spend a budget guessing
alichherawalla Aug 16, 2026
18be81a
fix(e2e): set enhancement inside the chat, which is the only place it…
alichherawalla Aug 16, 2026
b36675a
fix(e2e): wait for the sheet to close before tapping what is behind it
alichherawalla Aug 16, 2026
eb139af
fix(e2e): a late observer records a MISSED live state, it does not fail
alichherawalla Aug 16, 2026
6de1ea0
fix(e2e): race the live state against the finished image, do not wait…
alichherawalla Aug 16, 2026
1e1777f
feat(e2e): expose raw tap and window size on the sync surface
alichherawalla Aug 16, 2026
2b8f213
feat(e2e): a journey for three attachments on one message
alichherawalla Aug 16, 2026
c37903b
docs(gaps): the coverage we know is missing, before we forget it
alichherawalla Aug 16, 2026
fbdf435
docs(gaps): rank the untested paths, with the evidence for each
alichherawalla Aug 16, 2026
5fee383
docs(gaps): model memory is estimated unscientifically, and fails sil…
alichherawalla Aug 16, 2026
ad29469
fix(models): show the device's TOTAL memory, not what happens to be free
alichherawalla Aug 16, 2026
8f35f73
docs(gaps): how to fix the memory estimate - one owner, a real formul…
alichherawalla Aug 16, 2026
03c0ab7
docs: the memory and residency test matrix, covered and uncovered tog…
alichherawalla Aug 16, 2026
e8e1117
docs: relaunch residency is not a gap, it is the design
alichherawalla Aug 16, 2026
886510e
feat(stt): end a voice turn on silence instead of waiting for the sto…
alichherawalla Aug 16, 2026
2a466d7
test: onboarding shows total memory, not available
alichherawalla Aug 16, 2026
1f2e680
test(e2e): model residency, eviction and SPOKEN image-intent journeys…
alichherawalla Aug 16, 2026
85eded9
test(e2e): pin the text model in the multi-attachment journey
alichherawalla Aug 16, 2026
3457042
chore: bump pro for the durable shared-file control fix
alichherawalla Aug 16, 2026
5920339
refactor(stt): give the silence endpoint its own owner
alichherawalla Aug 16, 2026
7cd46db
fix(stt): start the silence clock when recording starts, not on the f…
alichherawalla Aug 16, 2026
b1fb750
fix(stt): end a voice turn on real silence, not on the transcript set…
alichherawalla Aug 16, 2026
2848027
fix(stt): silence must run the caller's stop path, not the service's
alichherawalla Aug 16, 2026
4f6ccd1
fix(stt): wire silence detection into the path the record button actu…
alichherawalla Aug 16, 2026
bf31c64
Revert "fix(stt): wire silence detection into the path the record but…
alichherawalla Aug 16, 2026
6e9950b
fix(audio): correct the onAudioReady option name (bufferLength, not b…
alichherawalla Aug 16, 2026
f5bc49d
feat(stt): observe silence in voice mode, behind a flag
alichherawalla Aug 16, 2026
7bc7a4c
feat(stt): end a voice turn on silence - verified on device
alichherawalla Aug 16, 2026
2cac3bb
feat(stt): a setting to turn stop-on-silence off
alichherawalla Aug 16, 2026
2dd48d2
refactor(stt): one owner for silence detection, and a proper home for…
alichherawalla Aug 16, 2026
0465d51
feat(chat): let a caller suppress assistant prose, keeping thinking a…
alichherawalla Aug 16, 2026
ae384ac
test(voice): a tool turn shows cards and speaks its narration, never …
alichherawalla Aug 16, 2026
193b252
fix(memory): one owner for "how much can we still allocate"
alichherawalla Aug 17, 2026
0806166
feat(logging): flush the debug log on demand
alichherawalla Aug 17, 2026
0662116
feat(diagnostics): say what startup is doing and what memory is doing…
alichherawalla Aug 17, 2026
f43482e
test(e2e): assert the transition, and let silence end the turn
alichherawalla Aug 17, 2026
7567d97
chore: bump pro for the sync, TTS and licensing fixes
alichherawalla Aug 17, 2026
bd75298
chore: keep internal helpers internal
alichherawalla Aug 17, 2026
aaf6887
chore: bump pro for the migration diagnostic
alichherawalla Aug 17, 2026
94f8f71
feat(voice): hands-free — the turn starts when you speak, not when yo…
alichherawalla Aug 17, 2026
848415b
voice: hands-free actually starts its own turns
alichherawalla Aug 17, 2026
89240ea
voice: talking over the assistant stops it (iOS)
alichherawalla Aug 17, 2026
6cbd28b
voice: echo cancellation on Android too, so barge-in is not iOS-only
alichherawalla Aug 17, 2026
2413be4
voice: the composer stops claiming to record while it waits
alichherawalla Aug 17, 2026
d209a32
voice: arming must not silence the assistant, or autoplay never happens
alichherawalla Aug 17, 2026
21b8978
voice: playback is audible again, and stop means stop
alichherawalla Aug 17, 2026
fb6588f
voice: voice notes start where the speech starts
alichherawalla Aug 17, 2026
4214739
voice: one deriver of record phase, one owner of echo cancellation
alichherawalla Aug 17, 2026
edcd130
docs: log the voice gaps and add 11 release-checklist rows
alichherawalla Aug 17, 2026
1d8dc19
voice: hands-free waits for the assistant, because we have no real ec…
alichherawalla Aug 17, 2026
0ebc5dd
revert the Oboe VoiceCommunication patch; correct the docs
alichherawalla Aug 17, 2026
0ecdc9f
voice: the assistant keeps the floor while it speaks
alichherawalla Aug 17, 2026
1ffa157
docs: log the real design gap - speak and listen are not serialized
alichherawalla Aug 17, 2026
2bf1dd5
voice: hands-free listens to the floor instead of polling for a gap
alichherawalla Aug 17, 2026
dfbad76
voice: make the trim cheap, and make it report its own cost
alichherawalla Aug 17, 2026
1bb0126
voice: let the room go quiet before opening the mic
alichherawalla Aug 17, 2026
ce0c772
voice: onset timings belong to ONE turn, or the trim cuts the wrong r…
alichherawalla Aug 17, 2026
e74465d
voice: full turn trace, and finaliseRecording becomes its own module
alichherawalla Aug 17, 2026
014b8fc
e2e: drive a voice turn from the Mac's own speaker and read the trace…
alichherawalla Aug 17, 2026
48095d8
voice: hands-free must not re-arm when a turn produced nothing
alichherawalla Aug 17, 2026
c79fb3e
voice: a turn must not hear the assistant it is waiting through
alichherawalla Aug 17, 2026
5548c68
voice: nothing opens a microphone without the lock; the observer is gone
alichherawalla Aug 17, 2026
a420277
e2e: two-device voice rig that reads the app's own log and never taps…
alichherawalla Aug 17, 2026
db89b5e
llm: stop writing llama.cpp's per-tensor log to disk during model load
alichherawalla Aug 17, 2026
1ff21aa
voice: hands-free must start when you ENTER the mode, not only when a…
alichherawalla Aug 17, 2026
0b591a2
voice: never queue behind ourselves, and never orphan the floor
alichherawalla Aug 17, 2026
b56866d
voice: one session replaces the lock, the floor and the flags
alichherawalla Aug 17, 2026
d6b4c7d
voice: recordingController stops being a second state machine
alichherawalla Aug 17, 2026
60eb1b2
voice: a tap is the way OUT of a stopped session
alichherawalla Aug 17, 2026
8f62f6c
voice: the two delays are settings, and the recorder obeys a replay
alichherawalla Aug 17, 2026
91ffc19
voice: a deliberate stop sticks, and notes lose their end-of-turn dea…
alichherawalla Aug 17, 2026
67d3447
docs: gap - a sender dying mid-batch leaves zombie receiving rows
alichherawalla Aug 17, 2026
90fd782
docs: gap - queued transfers neither follow a moved peer nor expire
alichherawalla Aug 17, 2026
2ec128f
chore: pro - image turns end their session turn; delivery rows obey t…
alichherawalla Aug 17, 2026
87b27c2
chore: pro - self-receive rows removed, departed-identity deliveries …
alichherawalla Aug 18, 2026
7106ae5
chore: pro - shared orphaned-deliveries rule
alichherawalla Aug 18, 2026
b5c7dfe
chore: pro - content-hash dedup for shared files
alichherawalla Aug 18, 2026
a4965b9
chore: pro - sender-side dedup, hash backfill, sync screen controls
alichherawalla Aug 18, 2026
ba33c31
chore: pro - audit cut 1
alichherawalla Aug 18, 2026
210073f
voice: one tap is one recording, not two
alichherawalla Aug 18, 2026
caf1414
voice tests: reset the session, and expect the phase a person actuall…
alichherawalla Aug 18, 2026
3ca35fd
sync tests: assert what the wire and the library actually carry
alichherawalla Aug 18, 2026
3bada54
docs: close two gaps, and log the device pass voice still owes
alichherawalla Aug 18, 2026
8314d8e
pro: durable control from the op-log, and one writer for a delivery row
alichherawalla Aug 18, 2026
2473575
ci: build @offgrid/speech, and test pro on the MATCHING branch
alichherawalla Aug 18, 2026
b016a84
pro: build @offgrid/speech in pro's CI too
alichherawalla Aug 18, 2026
69d8c17
test: stop the support-share sheet waits racing a CI runner
alichherawalla Aug 18, 2026
c981746
sync tests: cover the receive side, and stop two controls parsing as …
alichherawalla Aug 18, 2026
4317f50
pro: keep the content hash of the bytes this device holds
alichherawalla Aug 18, 2026
59ae22d
sync tests: the answer this phone gives a peer asking for a file back
alichherawalla Aug 18, 2026
0c82612
sync tests: a file arriving under a second identity is the same file
alichherawalla Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,37 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Check out pro submodule (private; skipped on open-core forks)
# When the PRO_SUBMODULE_PAT secret is present (this org's CI), pull the private pro/
# submodule so the pro-dependent suites run against the REAL package — a green run then
# actually exercises pro, not a stub. It also matters to the other gates: tsc sees
# __tests__/pro/* importing @offgrid/pro/* (TS2307 without it), and knip/depcruise need it
# or their reachability graph is incomplete and reports false orphans. Forks without the
# secret skip this: proExists=false and jest runs the open-core suite instead.
# Pull the private pro/ so the pro-dependent suites run against the REAL package — a green run
# then actually exercises pro, not a stub. It also matters to the other gates: tsc sees
# __tests__/pro/* importing @offgrid/pro/* (TS2307 without it), and knip/depcruise need it or
# their reachability graph is incomplete and reports false orphans. Forks without the secret skip
# this: proExists=false and jest runs the open-core suite instead.
#
# An explicit checkout of the MATCHING branch, not `git submodule update --init`. The submodule
# pointer is deliberately a pin - it records which pro revision a core commit was verified against
# - but a pin is the wrong thing for CI to test: a coordinated core+pro change would run this
# branch's core against whatever commit the pointer happens to hold, so the two halves of one
# change are never seen together. Matching branch first, main as the fallback, exactly as the
# shared checkout below and as OGAD does it.
- name: Check out the matching pro branch
if: ${{ env.PRO_SUBMODULE_PAT != '' }}
env:
PRO_PAT: ${{ env.PRO_SUBMODULE_PAT }}
run: |
git config --global url."https://x-access-token:${PRO_PAT}@github.com/".insteadOf "https://github.com/"
git submodule update --init --recursive pro
git config --global --unset url."https://x-access-token:${PRO_PAT}@github.com/".insteadOf || true
id: pro_branch
continue-on-error: true
uses: actions/checkout@v4
with:
repository: off-grid-ai/mobile-pro
token: ${{ env.PRO_SUBMODULE_PAT }}
path: pro
ref: ${{ github.head_ref || github.ref_name }}
persist-credentials: false
- name: Fall back to pro main
if: ${{ env.PRO_SUBMODULE_PAT != '' && steps.pro_branch.outcome != 'success' }}
uses: actions/checkout@v4
with:
repository: off-grid-ai/mobile-pro
token: ${{ env.PRO_SUBMODULE_PAT }}
path: pro
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@v4
Expand Down Expand Up @@ -117,6 +134,10 @@ jobs:
# reads src/, but tsc and metro read dist/). rag was never built at all before.
npm --prefix ../shared/packages/sync run build
npm --prefix ../shared/packages/rag run build
# speech too. It is a file: dep like the other two and six src files import it, but it was
# never built here - so every gate failed on "Cannot find module '@offgrid/speech'" (1966
# errors in one run) long before it reached anything real.
npm --prefix ../shared/packages/speech run build

- name: Install dependencies
id: install
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ fastlane/*.p8

# testing
/coverage
/.artifacts

# Yarn
.yarn
Expand All @@ -94,3 +95,6 @@ docs/DEVICE_SESSION_COMMENTARY.md

# Device wire-capture logs (raw, large, session-specific — kept locally, not versioned)
docs/wire-captures/

# Scratch renders derived from scripts/e2e/fixtures (regenerable, not versioned)
/tmp/
14 changes: 14 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { useAppStore, useAuthStore, useRemoteServerStore, useWhisperStore } from './src/stores';
import { useDebugLogsStore } from './src/stores/debugLogsStore';
import { initDebugLogFile, appendDebugLine } from './src/utils/debugLogFile';
import { startStartupMemoryProbe } from './src/services/startupMemoryProbe';
import { loadProFeatures } from './src/bootstrap/loadProFeatures';
import { hydrateDownloadStore } from './src/services/downloadHydration';
import { initActiveDownloadPersistence } from './src/services/activeDownloadPersistence';
Expand Down Expand Up @@ -56,6 +57,10 @@
logger.warn = tap('warn');
logger.error = tap('error');
initDebugLogFile();
// Immediately after the sink exists, so the first sample lands before anything heavy runs. The app
// was being killed by iOS at launch with the log going silent half a second in; this says where it
// stops and what memory was doing when it did.
startStartupMemoryProbe();
}

const ensureRemoteServerStoreHydrated = async () => {
Expand Down Expand Up @@ -193,17 +198,19 @@
await modelManager.reconcileFinishedImageDownloads(activeImageModelIds).catch((error) => {
logger.error('[App] Image model reconciliation failed:', error);
});
logger.log('[BOOT] refresh model lists');
const { textModels, imageModels } = await modelManager.refreshModelLists();
setDownloadedModels(textModels);
setDownloadedImageModels(imageModels);
})().catch((error) => {
logger.error('[App] Download-state recovery failed:', error);
});
}, [setDownloadedModels, setDownloadedImageModels]);

Check warning on line 208 in App.tsx

View workflow job for this annotation

GitHub Actions / ci

React Hook useCallback has a missing dependency: 'reattachTextDownloadRecovery'. Either include it or remove the dependency array

const initializeApp = useCallback(async () => {
try {
// Ensure persisted download metadata is loaded before restore logic reads it.
logger.log('[BOOT] app store hydrate');
await ensureAppStoreHydrated();

// Project the persisted "aggressive model loading" setting onto the residency
Expand All @@ -220,16 +227,19 @@

// Phase 1: Quick initialization - get app ready to show UI
// Initialize hardware detection
logger.log('[BOOT] device info');
const deviceInfo = await hardwareService.getDeviceInfo();
setDeviceInfo(deviceInfo);

const recommendation = hardwareService.getModelRecommendation();
setModelRecommendation(recommendation);

// Initialize model manager and load downloaded models list
logger.log('[BOOT] modelManager.initialize');
await modelManager.initialize();

// Clean up any mmproj files that were incorrectly added as standalone models
logger.log('[BOOT] cleanup mmproj entries');
await modelManager.cleanupMMProjEntries();

// Scan for any models that may have been downloaded externally or
Expand All @@ -243,6 +253,7 @@

// Ensure remote server store is hydrated before initializing providers,
// so getServers() / activeServerId reads see persisted data.
logger.log('[BOOT] remote server hydrate');
await ensureRemoteServerStoreHydrated();

// Initialize remote server providers in the background — don't block
Expand All @@ -252,6 +263,7 @@
});

// Check if passphrase is set and lock app if needed
logger.log('[BOOT] auth passphrase check');
const hasPassphrase = await authService.hasPassphrase();
if (hasPassphrase && authEnabled) {
setLocked(true);
Expand All @@ -265,12 +277,14 @@
// read, then activate only the capabilities that entitlement permits.
// loadProFeatures separately projects cached credential access from a
// Debug developer unlock; Sync reconciliation owns device admission.
logger.log('[BOOT] load pro features');
await loadProFeatures();
} catch (proError) {
logger.error('[App] Pro feature load failed, continuing without Pro:', proError);
}

// Show the UI immediately
logger.log('[BOOT] startup complete');
setIsInitializing(false);

// Reconcile downloaded Whisper models against disk at startup. presentModelIds
Expand Down
48 changes: 26 additions & 22 deletions __tests__/hardening/batch3-documentAttach.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,31 @@
* does not exhaustively assert.
*/

import RNFS from 'react-native-fs';
import { defaultNativeFileSystemBoundary } from '../harness/nativeFileSystem';

jest.mock('react-native-fs', () => {
const { defaultNativeFileSystemBoundary: boundary } = require('../harness/nativeFileSystem');
return { __esModule: true, default: boundary.module, ...boundary.module };
});

jest.mock('../../src/services/pdfExtractor', () => ({
pdfExtractor: { isAvailable: jest.fn(() => false), extractText: jest.fn() },
}));

import { documentService } from '../../src/services/documentService';

const rnfs = RNFS as jest.Mocked<typeof RNFS>;

/** Make RNFS behave as if `content` is a readable file of `size` bytes. */
function stubReadableFile(content: string, size = content.length): void {
rnfs.exists.mockResolvedValue(true);
rnfs.stat.mockResolvedValue({ size, isFile: () => true } as any);
rnfs.readFile.mockResolvedValue(content);
rnfs.copyFile.mockResolvedValue(undefined as any);
rnfs.mkdir.mockResolvedValue(undefined as any);
rnfs.unlink.mockResolvedValue(undefined as any);
/** Put one readable file on the fake device, with independent stored bytes and reported metadata. */
function seedReadableFile(
path: string,
content: string,
size = content.length,
): void {
defaultNativeFileSystemBoundary.seedTextFile(path, content, size);
}

describe('Batch3 · document attach validation (real documentService)', () => {
beforeEach(() => {
jest.clearAllMocks();
defaultNativeFileSystemBoundary.reset();
});

// ── #14/#15/#2 + csv/code: the full supported accept-set ───────────────────
Expand All @@ -62,7 +64,7 @@
});

it.each(acceptedNames)('processDocumentFromPath() builds a document attachment for %s', async (name) => {
stubReadableFile('sample body', 11);
seedReadableFile(`/docs/${name}`, 'sample body', 11);
const att = await documentService.processDocumentFromPath(`/docs/${name}`, name);
expect(att).not.toBeNull();
expect(att!.type).toBe('document');
Expand All @@ -84,7 +86,6 @@
});

it('processDocumentFromPath() throws an "Unsupported file type" error for .docx (no chip is added)', async () => {
stubReadableFile('ignored');
await expect(
documentService.processDocumentFromPath('/docs/report.docx', 'report.docx'),
).rejects.toThrow(/Unsupported file type/);
Expand All @@ -94,14 +95,14 @@
// ── #13: oversized (>5MB) file rejected with a visible error ────────────────
describe('oversized files are rejected (#13)', () => {
it('rejects a file at 5MB + 1 byte with a "too large" error', async () => {
stubReadableFile('x', 5 * 1024 * 1024 + 1);
seedReadableFile('/docs/huge.txt', 'x', 5 * 1024 * 1024 + 1);
await expect(
documentService.processDocumentFromPath('/docs/huge.txt', 'huge.txt'),
).rejects.toThrow(/too large/i);
});

it('accepts a file exactly at the 5MB boundary', async () => {
stubReadableFile('ok', 5 * 1024 * 1024);
seedReadableFile('/docs/limit.txt', 'ok', 5 * 1024 * 1024);
const att = await documentService.processDocumentFromPath('/docs/limit.txt', 'limit.txt');
expect(att).not.toBeNull();
});
Expand All @@ -120,8 +121,8 @@
// service); per hardening rules we do NOT edit src, so the correct-behavior
// assertion is skipped and the actual (buggy) behavior is pinned below.
describe('URL-encoded filename display decode (#17)', () => {
it.skip('BUG-FOUND: display fileName should be decoded (my%20notes.txt -> "my notes.txt")', async () => {

Check warning on line 124 in __tests__/hardening/batch3-documentAttach.test.ts

View workflow job for this annotation

GitHub Actions / ci

Tests should not be skipped
stubReadableFile('body');
seedReadableFile('/docs/my%20notes.txt', 'body');
const att = await documentService.processDocumentFromPath(
'/docs/my%20notes.txt',
'my%20notes.txt',
Expand All @@ -130,8 +131,8 @@
expect(att!.fileName).toBe('my notes.txt');
});

it.skip('pins ACTUAL behavior: the display fileName is returned un-decoded (documents the bug) — SKIP: do not enshrine the bug as passing; see the desired-behavior skip above', async () => {

Check warning on line 134 in __tests__/hardening/batch3-documentAttach.test.ts

View workflow job for this annotation

GitHub Actions / ci

Tests should not be skipped
stubReadableFile('body');
seedReadableFile('/docs/my%20notes.txt', 'body');
const att = await documentService.processDocumentFromPath(
'/docs/my%20notes.txt',
'my%20notes.txt',
Expand All @@ -143,9 +144,12 @@
it('resolves the file even when the PATH is URL-encoded (path decode works)', async () => {
// The path decode DOES happen (resolveContentUri), so a file whose path
// carries %20 still reads without error — the attach itself succeeds.
stubReadableFile('decoded path body');
seedReadableFile(
'/mock/documents/my notes.txt',
'decoded path body',
);
const att = await documentService.processDocumentFromPath(
'/docs/my%20notes.txt',
'/mock/documents/my%20notes.txt',
'my%20notes.txt',
);
expect(att).not.toBeNull();
Expand All @@ -156,14 +160,14 @@
// ── #34/#35: multiple document attachments queue as distinct attachments ────
describe('multiple document attachments queue (#34, #35)', () => {
it('produces two distinct attachments with unique ids for two files', async () => {
stubReadableFile('py body');
seedReadableFile('/docs/a.py', 'py body');
const first = await documentService.processDocumentFromPath('/docs/a.py', 'a.py');

// Advance the clock so the second attachment gets a different id (id is Date.now()).
const nowSpy = jest.spyOn(Date, 'now');
const base = Date.now();
nowSpy.mockReturnValue(base + 5);
rnfs.readFile.mockResolvedValue('ts body');
seedReadableFile('/docs/b.ts', 'ts body');
Comment thread
alichherawalla marked this conversation as resolved.
const second = await documentService.processDocumentFromPath('/docs/b.ts', 'b.ts');
nowSpy.mockRestore();

Expand Down
Loading
Loading