You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Candidates surfaced by what shipped in v2.31.0 — the sidecar metadata mode (#216) and the relative-time localisation. Filed at tag time as a tracker; v2.31.0 ships as it is.
Sidecar mode — cost and scale
The index is rewritten in full on every note save.writeNoteSidecar reads this device's entries, updates one, and writes the whole file back. On a folder of a thousand notes that is a ~150 KB read plus a ~150 KB write per autosave — once a second while typing — and a sync client watching the folder sees the index change each time. SidecarStore already skips a write whose bytes are unchanged, which only helps when nothing moved. An in-memory dirty set flushed on a coarser trigger would fix it; the honest reason it is not done yet is that the flush points need thinking about, since an unflushed entry is the Search results show same notes multiple times #140 duplicate again. Impact: sync traffic and battery on large folders — the setups this mode is aimed at.
Half of this is done in perf(sync): stop re-reading the sidecar index on every note save (#262) #266: this device's own index is now held in memory, so the read is gone and a lost-update window closed with it. The write is unchanged — SidecarStore.write still encodes every entry and replaces the whole file, so the ~150 KB write per autosave and the sync client seeing the index touched each time both stand. The dirty-set flush is what remains.
Every sidecar write and read lists the whole folder. Both writeNoteSidecar and SidecarStore.readAll call listFiles() on the SAF tree, which is the expensive call in this API. The frontmatter path grew MirrorFileCache for exactly this reason (Hardening candidates: v2.28.2 #222); the sidecar path has no equivalent. Impact: save latency proportional to folder size.
Reduced in perf(sync): stop re-reading the sidecar index on every note save (#262) #266, not closed. A save used to list the folder three times and parse every index twice; it now lists once in writeNoteSidecar (plus findFile inside SidecarStore.write, which walks the tree too). The merged view is consulted only for a note this device has not written before. Still proportional to folder size, just with a smaller constant — the MirrorFileCache equivalent does not exist.
A note deleted on another device leaves its entry in that device's index for ever. Nothing prunes entries whose note is gone, and deleteNote only touches our own. Two devices will each carry the other's stale rows. Harmless today — a stale entry is only consulted by filename, and a missing file matches nothing — but the file grows monotonically. Impact: index size drifts up over a folder's lifetime.
Sidecar mode — correctness edges
Adoption compares filenames case-insensitively, but a case-sensitive filesystem can hold both.adoptUnclaimedFile and the recorded-name lookup both use equals(ignoreCase = true), which is right for exFAT and a Windows share and wrong for ext4, where Notes.md and notes.md are two files. The frontmatter path made the same choice deliberately (Notes get saved several times #213) and inherits the same edge. Impact: on a Linux-backed sync, a note could adopt the wrong file of a case-differing pair.
A conflict copy records the remote hash under the original note's entry. That is what stops the copy being taken again every pass — the sidecar equivalent of remoteSeenAt — but it means the entry briefly describes content the local note does not have. Correct, and documented, yet it makes contentHash mean "last wrote or accepted" rather than "last wrote". Worth a second look if anything else ever reads that field. Impact: none observed; a trap for future readers.
Switching modes is not resumable.SidecarMigration walks the folder rewriting files; a process death halfway leaves some files converted and some not. The import path tolerates that (it strips a stray header and uses its id), so the folder is not corrupt — but the setting has already flipped or not, and nothing reports which files were missed. Impact: a half-converted folder with no way to see it, on a switch over a large folder.
Localisation
Nothing prevents the next hardcoded string. The relative-time labels were Korean in every locale for as long as they existed, in two separate copies, and were found only because someone went looking for something else. UntranslatedStringTest and ResourceParityTest check that resources exist and agree across locales; neither can see a literal that never became a resource. A lint rule or a source scan for user-facing literals would. Impact: the same defect class recurs silently — this was its third instance after Sync reconcile: conflict copies multiply every pass, and four related mirror defects #217.
Done in test: fail the build on user-facing text that never became a resource (#262) #263. HardcodedStringTest scans src/main for the two shapes the defect took: Hangul in any source literal (which is what would have caught formatRelative, whose literals never went near a UI call), and a literal handed straight to Text(), text = or contentDescription =. Both allowlists name real exceptions, and a third test fails once an entry stops matching, so neither list can rot into a blanket exemption.
Release process
Store notes overran the 500-character Play cap for the third release running.Hardening candidates: v2.26.1 #167, then de/es/fr in v2.30.0, then en-US here. verify-release-notes.ps1 catches it, but only after the files are written, and it warns below 50 characters of headroom — four of six locales are in that band right now. Authoring them against a live count, or shortening the house style for these notes, would stop the rewrite cycle. Impact: an avoidable retry at every cut.
launch-smoke fails on emulator install often enough to be noise. Three of the last four runs failed with Failure calling service package: Broken pipe or Can't find service: package before the app was ever installed, and passed on a straight re-run. It is deliberately non-required so it does not block a merge, but a check that flakes this often trains people to ignore it. Worth either hardening the install step or dropping the job. Impact: a red check that carries no information.
The evidence has moved since this was written (measured 2026-07-24, last 6 android-build runs): launch-smoke passed 4, failed 1, did not run once. Hardening candidates: v2.29.1 #252 already noted two clean runs in a row; this is more of the same, so "three of the last four" no longer describes the job.
Candidates surfaced by what shipped in v2.31.0 — the sidecar metadata mode (#216) and the relative-time localisation. Filed at tag time as a tracker; v2.31.0 ships as it is.
Sidecar mode — cost and scale
writeNoteSidecarreads this device's entries, updates one, and writes the whole file back. On a folder of a thousand notes that is a ~150 KB read plus a ~150 KB write per autosave — once a second while typing — and a sync client watching the folder sees the index change each time.SidecarStorealready skips a write whose bytes are unchanged, which only helps when nothing moved. An in-memory dirty set flushed on a coarser trigger would fix it; the honest reason it is not done yet is that the flush points need thinking about, since an unflushed entry is the Search results show same notes multiple times #140 duplicate again. Impact: sync traffic and battery on large folders — the setups this mode is aimed at.SidecarStore.writestill encodes every entry and replaces the whole file, so the ~150 KB write per autosave and the sync client seeing the index touched each time both stand. The dirty-set flush is what remains.writeNoteSidecarandSidecarStore.readAllcalllistFiles()on the SAF tree, which is the expensive call in this API. The frontmatter path grewMirrorFileCachefor exactly this reason (Hardening candidates: v2.28.2 #222); the sidecar path has no equivalent. Impact: save latency proportional to folder size.writeNoteSidecar(plusfindFileinsideSidecarStore.write, which walks the tree too). The merged view is consulted only for a note this device has not written before. Still proportional to folder size, just with a smaller constant — theMirrorFileCacheequivalent does not exist.deleteNoteonly touches our own. Two devices will each carry the other's stale rows. Harmless today — a stale entry is only consulted by filename, and a missing file matches nothing — but the file grows monotonically. Impact: index size drifts up over a folder's lifetime.Sidecar mode — correctness edges
adoptUnclaimedFileand the recorded-name lookup both useequals(ignoreCase = true), which is right for exFAT and a Windows share and wrong for ext4, whereNotes.mdandnotes.mdare two files. The frontmatter path made the same choice deliberately (Notes get saved several times #213) and inherits the same edge. Impact: on a Linux-backed sync, a note could adopt the wrong file of a case-differing pair.remoteSeenAt— but it means the entry briefly describes content the local note does not have. Correct, and documented, yet it makescontentHashmean "last wrote or accepted" rather than "last wrote". Worth a second look if anything else ever reads that field. Impact: none observed; a trap for future readers.SidecarMigrationwalks the folder rewriting files; a process death halfway leaves some files converted and some not. The import path tolerates that (it strips a stray header and uses its id), so the folder is not corrupt — but the setting has already flipped or not, and nothing reports which files were missed. Impact: a half-converted folder with no way to see it, on a switch over a large folder.Localisation
UntranslatedStringTestandResourceParityTestcheck that resources exist and agree across locales; neither can see a literal that never became a resource. A lint rule or a source scan for user-facing literals would. Impact: the same defect class recurs silently — this was its third instance after Sync reconcile: conflict copies multiply every pass, and four related mirror defects #217.HardcodedStringTestscanssrc/mainfor the two shapes the defect took: Hangul in any source literal (which is what would have caughtformatRelative, whose literals never went near a UI call), and a literal handed straight toText(),text =orcontentDescription =. Both allowlists name real exceptions, and a third test fails once an entry stops matching, so neither list can rot into a blanket exemption.Release process
verify-release-notes.ps1catches it, but only after the files are written, and it warns below 50 characters of headroom — four of six locales are in that band right now. Authoring them against a live count, or shortening the house style for these notes, would stop the rewrite cycle. Impact: an avoidable retry at every cut.launch-smokefails on emulator install often enough to be noise. Three of the last four runs failed withFailure calling service package: Broken pipeorCan't find service: packagebefore the app was ever installed, and passed on a straight re-run. It is deliberately non-required so it does not block a merge, but a check that flakes this often trains people to ignore it. Worth either hardening the install step or dropping the job. Impact: a red check that carries no information.android-buildruns):launch-smokepassed 4, failed 1, did not run once. Hardening candidates: v2.29.1 #252 already noted two clean runs in a row; this is more of the same, so "three of the last four" no longer describes the job.instrumented-tests, added in test: make the instrumented suite run, and run it in CI (#235, #239) #272 and failing 3 of those same 6 runs. Both jobs arecontinue-on-error, so the emulator path still gates nothing — but the job worth diagnosing is now the new one. Tracked here rather than in a new issue; the same item appears in Hardening candidates: CI artifact retention and release assets #247 and Hardening candidates: v2.29.1 #252.