diff --git a/.github/workflows/fdroid.yml b/.github/workflows/fdroid.yml index aeb9eb51..d18795cf 100644 --- a/.github/workflows/fdroid.yml +++ b/.github/workflows/fdroid.yml @@ -148,9 +148,12 @@ jobs: run: uv run --directory fdroid python3 scan_apks.py repo/ - name: Seed repo icon - run: | - mkdir -p fdroid/repo/icons - cp fdroid/icons/icon.png fdroid/repo/icons/icon.png + # `fdroid update` runs with cwd=fdroid/ (via --directory) and resolves the + # default repo_icon config value ("icon.png") relative to that cwd, then + # copies it into repo/icons/ itself. It must exist at fdroid/icon.png, not + # fdroid/repo/icons/icon.png — a mismatch here silently falls back to a + # generated QR-code placeholder instead of erroring. + run: cp fdroid/icons/icon.png fdroid/icon.png - name: Generate fdroid index run: uv run --directory fdroid fdroid update --create-metadata --verbose diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d7203492..52ffe5a9 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.73.0" + ".": "0.74.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c7835e..ff8fa5d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [0.74.0](https://github.com/tstapler/stelekit/compare/v0.73.0...v0.74.0) (2026-08-23) + + +### Features + +* fix paranoid-mode DB sync gap, add git commit to settings version ([b8dcae6](https://github.com/tstapler/stelekit/commit/b8dcae6069ff8377ef44c173e42f16dff061fc92)) +* **sync:** shared event-driven rescan scheduler for Android and web ([8e8eff4](https://github.com/tstapler/stelekit/commit/8e8eff489070949393fa293cc0d6163d254fce11)) + + +### Bug Fixes + +* **android:** heal today-journal duplicate from warm-start Phase-1 race ([d38dd92](https://github.com/tstapler/stelekit/commit/d38dd92a68bd5e9ea8f59cd38dcf7db0e720f686)) +* **db:** emit write-actor invalidation after op() completes, not before ([74afc62](https://github.com/tstapler/stelekit/commit/74afc62fd5e37357cfe73880fe1ac8b2c2ac2a0e)) +* **parser:** sanitize restricted control chars instead of aborting page load ([#274](https://github.com/tstapler/stelekit/issues/274)) ([1528d06](https://github.com/tstapler/stelekit/commit/1528d06500fe52842787c367b2398000640f6fb2)) +* **test:** make DiskConflictResolutionTest rename assertion deterministic ([#277](https://github.com/tstapler/stelekit/issues/277)) ([4be9651](https://github.com/tstapler/stelekit/commit/4be9651462bb38e778e509112712f7b3280dc64b)) +* **test:** synchronize on coordinator events, not flow completion ([#276](https://github.com/tstapler/stelekit/issues/276)) ([c4bf074](https://github.com/tstapler/stelekit/commit/c4bf074d52b7242cf658386307a247ac54794981)) + ## [0.73.0](https://github.com/tstapler/stelekit/compare/v0.72.1...v0.73.0) (2026-08-18) diff --git a/Casks/stelekit.rb b/Casks/stelekit.rb index 4ccad042..d610b248 100644 --- a/Casks/stelekit.rb +++ b/Casks/stelekit.rb @@ -1,7 +1,7 @@ # Cask managed by the release workflow — do not edit sha256/version manually. cask "stelekit" do - version "0.73.0" - sha256 "a4f120315b1c23b74829e37f69a9d9695e12655e40c9615e31ba1faf6b64ecc8" + version "0.74.0" + sha256 "dd61538232f0ed229df3bece041672503e82040c8d2d98ccf52eb8791cc0bf97" url "https://github.com/tstapler/stelekit/releases/download/v#{version}/SteleKit-v#{version}-macos.dmg" name "SteleKit" diff --git a/Formula/stelekit.rb b/Formula/stelekit.rb index 605c2c1e..55aec780 100644 --- a/Formula/stelekit.rb +++ b/Formula/stelekit.rb @@ -2,16 +2,16 @@ class Stelekit < Formula desc "Markdown-based outliner and note-taking app (Kotlin Multiplatform)" homepage "https://github.com/tstapler/stelekit" - version "0.73.0" + version "0.74.0" on_linux do url "https://github.com/tstapler/stelekit/releases/download/v#{version}/SteleKit-v#{version}-linux.AppImage" - sha256 "260889f68eb6409743d8480f77ba26c3806feca355e28617da78bc4e8091c6d4" + sha256 "c23dc9df35869f5c3031a7806d4c2bea388a93fa21afeda2f9ad3f30ca0b6360" end on_macos do url "https://github.com/tstapler/stelekit/releases/download/v#{version}/SteleKit-v#{version}-macos.dmg" - sha256 "a4f120315b1c23b74829e37f69a9d9695e12655e40c9615e31ba1faf6b64ecc8" + sha256 "dd61538232f0ed229df3bece041672503e82040c8d2d98ccf52eb8791cc0bf97" end def install diff --git a/benchmarks/android-history/2026-08-24_07h39m07s_c7f35a1d.json b/benchmarks/android-history/2026-08-24_07h39m07s_c7f35a1d.json new file mode 100644 index 00000000..7112ef5a --- /dev/null +++ b/benchmarks/android-history/2026-08-24_07h39m07s_c7f35a1d.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2026-08-24 07:39:07 UTC", + "gitSha": "c7f35a1d", + "branch": "main", + "phase1Ms": 50, + "phase3Ms": 4218, + "pageCount": 530, + "baselineP95Ms": 9, + "phase3P95Ms": 15, + "jankFactor": 1.67, + "writes": 21, + "safDirectMs": 0, + "safProviderMs": 6, + "safPerFileMs": 0.2, + "safDirectPerFileMs": 0.0, + "safOverhead": 6.0 +} \ No newline at end of file diff --git a/benchmarks/android-history/2026-08-24_07h45m22s_55827713.json b/benchmarks/android-history/2026-08-24_07h45m22s_55827713.json new file mode 100644 index 00000000..422bfdb9 --- /dev/null +++ b/benchmarks/android-history/2026-08-24_07h45m22s_55827713.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2026-08-24 07:45:22 UTC", + "gitSha": "55827713", + "branch": "main", + "phase1Ms": 33, + "phase3Ms": 3898, + "pageCount": 530, + "baselineP95Ms": 12, + "phase3P95Ms": 12, + "jankFactor": 1.0, + "writes": 20, + "safDirectMs": 0, + "safProviderMs": 7, + "safPerFileMs": 0.23, + "safDirectPerFileMs": 0.0, + "safOverhead": 7.0 +} \ No newline at end of file diff --git a/benchmarks/android-history/2026-08-25_04h39m25s_1bd4d2ca.json b/benchmarks/android-history/2026-08-25_04h39m25s_1bd4d2ca.json new file mode 100644 index 00000000..672f8fa9 --- /dev/null +++ b/benchmarks/android-history/2026-08-25_04h39m25s_1bd4d2ca.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2026-08-25 04:39:25 UTC", + "gitSha": "1bd4d2ca", + "branch": "main", + "phase1Ms": 40, + "phase3Ms": 3833, + "pageCount": 530, + "baselineP95Ms": 8, + "phase3P95Ms": 15, + "jankFactor": 1.88, + "writes": 19, + "safDirectMs": 1, + "safProviderMs": 5, + "safPerFileMs": 0.17, + "safDirectPerFileMs": 0.03, + "safOverhead": 5.0 +} \ No newline at end of file diff --git a/benchmarks/android-history/2026-08-26_20h34m34s_1efdd771.json b/benchmarks/android-history/2026-08-26_20h34m34s_1efdd771.json new file mode 100644 index 00000000..d1237306 --- /dev/null +++ b/benchmarks/android-history/2026-08-26_20h34m34s_1efdd771.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2026-08-26 20:34:34 UTC", + "gitSha": "1efdd771", + "branch": "main", + "phase1Ms": 34, + "phase3Ms": 3784, + "pageCount": 530, + "baselineP95Ms": 9, + "phase3P95Ms": 21, + "jankFactor": 2.33, + "writes": 18, + "safDirectMs": 0, + "safProviderMs": 7, + "safPerFileMs": 0.23, + "safDirectPerFileMs": 0.0, + "safOverhead": 7.0 +} \ No newline at end of file diff --git a/benchmarks/android-history/2026-08-26_20h41m59s_5dbf0150.json b/benchmarks/android-history/2026-08-26_20h41m59s_5dbf0150.json new file mode 100644 index 00000000..d6776ce3 --- /dev/null +++ b/benchmarks/android-history/2026-08-26_20h41m59s_5dbf0150.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2026-08-26 20:41:59 UTC", + "gitSha": "5dbf0150", + "branch": "main", + "phase1Ms": 33, + "phase3Ms": 4048, + "pageCount": 530, + "baselineP95Ms": 8, + "phase3P95Ms": 13, + "jankFactor": 1.63, + "writes": 20, + "safDirectMs": 0, + "safProviderMs": 9, + "safPerFileMs": 0.3, + "safDirectPerFileMs": 0.0, + "safOverhead": 9.0 +} \ No newline at end of file diff --git a/benchmarks/android-history/2026-08-27_00h26m51s_7becc238.json b/benchmarks/android-history/2026-08-27_00h26m51s_7becc238.json new file mode 100644 index 00000000..16877ff0 --- /dev/null +++ b/benchmarks/android-history/2026-08-27_00h26m51s_7becc238.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2026-08-27 00:26:51 UTC", + "gitSha": "7becc238", + "branch": "main", + "phase1Ms": 36, + "phase3Ms": 3674, + "pageCount": 530, + "baselineP95Ms": 7, + "phase3P95Ms": 12, + "jankFactor": 1.71, + "writes": 18, + "safDirectMs": 0, + "safProviderMs": 4, + "safPerFileMs": 0.13, + "safDirectPerFileMs": 0.0, + "safOverhead": 4.0 +} \ No newline at end of file diff --git a/benchmarks/history/2026-08-24_07h35m31s_c7f35a1d.json b/benchmarks/history/2026-08-24_07h35m31s_c7f35a1d.json new file mode 100644 index 00000000..ea01ab44 --- /dev/null +++ b/benchmarks/history/2026-08-24_07h35m31s_c7f35a1d.json @@ -0,0 +1,122 @@ +{ + "timestamp": "2026-08-24 07:35:31 UTC", + "gitSha": "c7f35a1d", + "branch": "main", + "graphConfig": "xlarge", + "pageCount": 230, + "journalCount": 30, + "phase1TtiMs": 1, + "phase2Ms": 0, + "phase3Ms": 1, + "totalMs": 2, + "jankBaselineP50Ms": 1, + "jankBaselineP95Ms": 24, + "jankLoadP50Ms": -1, + "jankLoadP95Ms": -1, + "jankFactor": -0.04, + "allocHotspots": [ + { + "frame": "byte[]_[k]", + "samples": 588, + "pct": 38.0 + }, + { + "frame": "java.lang.String_[k]", + "samples": 114, + "pct": 7.4 + }, + { + "frame": "int[]_[k]", + "samples": 94, + "pct": 6.1 + }, + { + "frame": "java.util.LinkedHashMap$Entry_[k]", + "samples": 86, + "pct": 5.6 + }, + { + "frame": "java.lang.Object[]_[k]", + "samples": 56, + "pct": 3.6 + }, + { + "frame": "java.lang.StringBuilder_[k]", + "samples": 42, + "pct": 2.7 + }, + { + "frame": "dev.stapler.stelekit.parsing.lexer.Token_[k]", + "samples": 35, + "pct": 2.3 + }, + { + "frame": "java.lang.String[]_[k]", + "samples": 33, + "pct": 2.1 + }, + { + "frame": "java.nio.DirectByteBuffer_[k]", + "samples": 32, + "pct": 2.1 + }, + { + "frame": "java.util.HashMap$Node[]_[k]", + "samples": 29, + "pct": 1.9 + } + ], + "cpuHotspots": [ + { + "frame": "/usr/lib/x86_64-linux-gnu/libc.so.6", + "samples": 31579, + "pct": 97.4 + }, + { + "frame": "/tmp/sqlite-3.51.3.0-d6361724-2336-43b3-abc6-14d0d0c8d13d-libsqlitejdbc.so", + "samples": 269, + "pct": 0.8 + }, + { + "frame": "fsync", + "samples": 140, + "pct": 0.4 + }, + { + "frame": "__libc_pwrite", + "samples": 80, + "pct": 0.2 + }, + { + "frame": "SR_handler", + "samples": 44, + "pct": 0.1 + }, + { + "frame": "pthread_cond_signal", + "samples": 36, + "pct": 0.1 + }, + { + "frame": "pread", + "samples": 13, + "pct": 0.0 + }, + { + "frame": "syscall", + "samples": 8, + "pct": 0.0 + }, + { + "frame": "malloc", + "samples": 7, + "pct": 0.0 + }, + { + "frame": "dev/stapler/stelekit/util/ContentHasher.sha256_[1]", + "samples": 5, + "pct": 0.0 + } + ], + "queryStats": [] +} \ No newline at end of file diff --git a/benchmarks/history/2026-08-24_07h44m54s_55827713.json b/benchmarks/history/2026-08-24_07h44m54s_55827713.json new file mode 100644 index 00000000..b2936237 --- /dev/null +++ b/benchmarks/history/2026-08-24_07h44m54s_55827713.json @@ -0,0 +1,122 @@ +{ + "timestamp": "2026-08-24 07:44:54 UTC", + "gitSha": "55827713", + "branch": "main", + "graphConfig": "xlarge", + "pageCount": 230, + "journalCount": 30, + "phase1TtiMs": 1, + "phase2Ms": 0, + "phase3Ms": 1, + "totalMs": 2, + "jankBaselineP50Ms": 1, + "jankBaselineP95Ms": 20, + "jankLoadP50Ms": -1, + "jankLoadP95Ms": -1, + "jankFactor": -0.05, + "allocHotspots": [ + { + "frame": "byte[]_[k]", + "samples": 597, + "pct": 37.9 + }, + { + "frame": "java.lang.String_[k]", + "samples": 106, + "pct": 6.7 + }, + { + "frame": "java.util.LinkedHashMap$Entry_[k]", + "samples": 100, + "pct": 6.3 + }, + { + "frame": "int[]_[k]", + "samples": 95, + "pct": 6.0 + }, + { + "frame": "java.lang.Object[]_[k]", + "samples": 51, + "pct": 3.2 + }, + { + "frame": "java.lang.StringBuilder_[k]", + "samples": 48, + "pct": 3.0 + }, + { + "frame": "java.nio.DirectByteBuffer_[k]", + "samples": 39, + "pct": 2.5 + }, + { + "frame": "dev.stapler.stelekit.parsing.lexer.Token_[k]", + "samples": 36, + "pct": 2.3 + }, + { + "frame": "java.util.Arrays$ArrayItr_[k]", + "samples": 28, + "pct": 1.8 + }, + { + "frame": "java.util.LinkedHashMap_[k]", + "samples": 27, + "pct": 1.7 + } + ], + "cpuHotspots": [ + { + "frame": "/usr/lib/x86_64-linux-gnu/libc.so.6", + "samples": 39759, + "pct": 97.9 + }, + { + "frame": "fsync", + "samples": 249, + "pct": 0.6 + }, + { + "frame": "/tmp/sqlite-3.51.3.0-d3bff224-8a76-4cd3-aa33-12c5d33e50db-libsqlitejdbc.so", + "samples": 219, + "pct": 0.5 + }, + { + "frame": "__libc_pwrite", + "samples": 132, + "pct": 0.3 + }, + { + "frame": "SR_handler", + "samples": 26, + "pct": 0.1 + }, + { + "frame": "pthread_cond_signal", + "samples": 23, + "pct": 0.1 + }, + { + "frame": "munmap", + "samples": 11, + "pct": 0.0 + }, + { + "frame": "malloc", + "samples": 8, + "pct": 0.0 + }, + { + "frame": "pread", + "samples": 8, + "pct": 0.0 + }, + { + "frame": "__libc_free", + "samples": 5, + "pct": 0.0 + } + ], + "queryStats": [] +} \ No newline at end of file diff --git a/benchmarks/history/2026-08-25_04h35m05s_1bd4d2ca.json b/benchmarks/history/2026-08-25_04h35m05s_1bd4d2ca.json new file mode 100644 index 00000000..9dbebbc3 --- /dev/null +++ b/benchmarks/history/2026-08-25_04h35m05s_1bd4d2ca.json @@ -0,0 +1,122 @@ +{ + "timestamp": "2026-08-25 04:35:05 UTC", + "gitSha": "1bd4d2ca", + "branch": "main", + "graphConfig": "xlarge", + "pageCount": 230, + "journalCount": 30, + "phase1TtiMs": 1, + "phase2Ms": 0, + "phase3Ms": 1, + "totalMs": 2, + "jankBaselineP50Ms": 1, + "jankBaselineP95Ms": 16, + "jankLoadP50Ms": -1, + "jankLoadP95Ms": -1, + "jankFactor": -0.06, + "allocHotspots": [ + { + "frame": "byte[]_[k]", + "samples": 636, + "pct": 38.2 + }, + { + "frame": "java.lang.String_[k]", + "samples": 123, + "pct": 7.4 + }, + { + "frame": "int[]_[k]", + "samples": 116, + "pct": 7.0 + }, + { + "frame": "java.util.LinkedHashMap$Entry_[k]", + "samples": 93, + "pct": 5.6 + }, + { + "frame": "java.lang.Object[]_[k]", + "samples": 61, + "pct": 3.7 + }, + { + "frame": "java.lang.StringBuilder_[k]", + "samples": 59, + "pct": 3.5 + }, + { + "frame": "java.nio.DirectByteBuffer_[k]", + "samples": 46, + "pct": 2.8 + }, + { + "frame": "dev.stapler.stelekit.parsing.lexer.Token_[k]", + "samples": 34, + "pct": 2.0 + }, + { + "frame": "java.util.HashMap$Node[]_[k]", + "samples": 30, + "pct": 1.8 + }, + { + "frame": "java.util.regex.Matcher_[k]", + "samples": 29, + "pct": 1.7 + } + ], + "cpuHotspots": [ + { + "frame": "/usr/lib/x86_64-linux-gnu/libc.so.6", + "samples": 35092, + "pct": 97.5 + }, + { + "frame": "/tmp/sqlite-3.51.3.0-a17a24c6-b3f6-4ada-bba5-5a365bff8e0a-libsqlitejdbc.so", + "samples": 356, + "pct": 1.0 + }, + { + "frame": "__libc_pwrite", + "samples": 125, + "pct": 0.3 + }, + { + "frame": "fsync", + "samples": 62, + "pct": 0.2 + }, + { + "frame": "SR_handler", + "samples": 51, + "pct": 0.1 + }, + { + "frame": "pthread_cond_signal", + "samples": 41, + "pct": 0.1 + }, + { + "frame": "malloc", + "samples": 14, + "pct": 0.0 + }, + { + "frame": "pread", + "samples": 10, + "pct": 0.0 + }, + { + "frame": "syscall", + "samples": 10, + "pct": 0.0 + }, + { + "frame": "prctl", + "samples": 8, + "pct": 0.0 + } + ], + "queryStats": [] +} \ No newline at end of file diff --git a/benchmarks/history/2026-08-26_20h32m37s_1efdd771.json b/benchmarks/history/2026-08-26_20h32m37s_1efdd771.json new file mode 100644 index 00000000..d2a19852 --- /dev/null +++ b/benchmarks/history/2026-08-26_20h32m37s_1efdd771.json @@ -0,0 +1,122 @@ +{ + "timestamp": "2026-08-26 20:32:37 UTC", + "gitSha": "1efdd771", + "branch": "main", + "graphConfig": "xlarge", + "pageCount": 230, + "journalCount": 30, + "phase1TtiMs": 1, + "phase2Ms": 0, + "phase3Ms": 1, + "totalMs": 2, + "jankBaselineP50Ms": 1, + "jankBaselineP95Ms": 18, + "jankLoadP50Ms": 1, + "jankLoadP95Ms": 1, + "jankFactor": 0.06, + "allocHotspots": [ + { + "frame": "byte[]_[k]", + "samples": 619, + "pct": 37.3 + }, + { + "frame": "java.lang.String_[k]", + "samples": 132, + "pct": 8.0 + }, + { + "frame": "java.util.LinkedHashMap$Entry_[k]", + "samples": 109, + "pct": 6.6 + }, + { + "frame": "int[]_[k]", + "samples": 94, + "pct": 5.7 + }, + { + "frame": "java.lang.Object[]_[k]", + "samples": 62, + "pct": 3.7 + }, + { + "frame": "java.lang.StringBuilder_[k]", + "samples": 52, + "pct": 3.1 + }, + { + "frame": "dev.stapler.stelekit.parsing.lexer.Token_[k]", + "samples": 47, + "pct": 2.8 + }, + { + "frame": "java.nio.DirectByteBuffer_[k]", + "samples": 42, + "pct": 2.5 + }, + { + "frame": "java.util.Arrays$ArrayItr_[k]", + "samples": 39, + "pct": 2.4 + }, + { + "frame": "java.util.regex.Matcher_[k]", + "samples": 29, + "pct": 1.7 + } + ], + "cpuHotspots": [ + { + "frame": "/usr/lib/x86_64-linux-gnu/libc.so.6", + "samples": 46315, + "pct": 97.7 + }, + { + "frame": "/tmp/sqlite-3.51.3.0-66ccfc9a-ccb8-4900-979d-69cb350a5698-libsqlitejdbc.so", + "samples": 404, + "pct": 0.9 + }, + { + "frame": "__libc_pwrite", + "samples": 149, + "pct": 0.3 + }, + { + "frame": "fsync", + "samples": 71, + "pct": 0.1 + }, + { + "frame": "clock_nanosleep", + "samples": 45, + "pct": 0.1 + }, + { + "frame": "SR_handler", + "samples": 42, + "pct": 0.1 + }, + { + "frame": "pthread_cond_signal", + "samples": 38, + "pct": 0.1 + }, + { + "frame": "pread", + "samples": 19, + "pct": 0.0 + }, + { + "frame": "syscall", + "samples": 10, + "pct": 0.0 + }, + { + "frame": "malloc", + "samples": 9, + "pct": 0.0 + } + ], + "queryStats": [] +} \ No newline at end of file diff --git a/benchmarks/history/2026-08-26_20h39m28s_5dbf0150.json b/benchmarks/history/2026-08-26_20h39m28s_5dbf0150.json new file mode 100644 index 00000000..660d85cf --- /dev/null +++ b/benchmarks/history/2026-08-26_20h39m28s_5dbf0150.json @@ -0,0 +1,122 @@ +{ + "timestamp": "2026-08-26 20:39:28 UTC", + "gitSha": "5dbf0150", + "branch": "main", + "graphConfig": "xlarge", + "pageCount": 230, + "journalCount": 30, + "phase1TtiMs": 1, + "phase2Ms": 0, + "phase3Ms": 1, + "totalMs": 2, + "jankBaselineP50Ms": 1, + "jankBaselineP95Ms": 19, + "jankLoadP50Ms": -1, + "jankLoadP95Ms": -1, + "jankFactor": -0.05, + "allocHotspots": [ + { + "frame": "byte[]_[k]", + "samples": 623, + "pct": 38.8 + }, + { + "frame": "java.lang.String_[k]", + "samples": 112, + "pct": 7.0 + }, + { + "frame": "int[]_[k]", + "samples": 104, + "pct": 6.5 + }, + { + "frame": "java.util.LinkedHashMap$Entry_[k]", + "samples": 85, + "pct": 5.3 + }, + { + "frame": "java.lang.StringBuilder_[k]", + "samples": 60, + "pct": 3.7 + }, + { + "frame": "java.lang.Object[]_[k]", + "samples": 53, + "pct": 3.3 + }, + { + "frame": "dev.stapler.stelekit.parsing.lexer.Token_[k]", + "samples": 42, + "pct": 2.6 + }, + { + "frame": "java.nio.DirectByteBuffer_[k]", + "samples": 31, + "pct": 1.9 + }, + { + "frame": "java.util.HashMap$Node[]_[k]", + "samples": 30, + "pct": 1.9 + }, + { + "frame": "java.lang.String[]_[k]", + "samples": 24, + "pct": 1.5 + } + ], + "cpuHotspots": [ + { + "frame": "/usr/lib/x86_64-linux-gnu/libc.so.6", + "samples": 45126, + "pct": 97.7 + }, + { + "frame": "/tmp/sqlite-3.51.3.0-75447252-36a5-48cb-9d9e-9eda5e5415a8-libsqlitejdbc.so", + "samples": 277, + "pct": 0.6 + }, + { + "frame": "clock_nanosleep", + "samples": 176, + "pct": 0.4 + }, + { + "frame": "fsync", + "samples": 152, + "pct": 0.3 + }, + { + "frame": "__libc_pwrite", + "samples": 80, + "pct": 0.2 + }, + { + "frame": "SR_handler", + "samples": 63, + "pct": 0.1 + }, + { + "frame": "pthread_cond_signal", + "samples": 40, + "pct": 0.1 + }, + { + "frame": "pread", + "samples": 13, + "pct": 0.0 + }, + { + "frame": "malloc", + "samples": 11, + "pct": 0.0 + }, + { + "frame": "__xstat64", + "samples": 6, + "pct": 0.0 + } + ], + "queryStats": [] +} \ No newline at end of file diff --git a/benchmarks/history/2026-08-27_00h25m15s_7becc238.json b/benchmarks/history/2026-08-27_00h25m15s_7becc238.json new file mode 100644 index 00000000..a3dfca25 --- /dev/null +++ b/benchmarks/history/2026-08-27_00h25m15s_7becc238.json @@ -0,0 +1,122 @@ +{ + "timestamp": "2026-08-27 00:25:15 UTC", + "gitSha": "7becc238", + "branch": "main", + "graphConfig": "xlarge", + "pageCount": 230, + "journalCount": 30, + "phase1TtiMs": 1, + "phase2Ms": 0, + "phase3Ms": 2, + "totalMs": 3, + "jankBaselineP50Ms": 1, + "jankBaselineP95Ms": 15, + "jankLoadP50Ms": 1, + "jankLoadP95Ms": 1, + "jankFactor": 0.07, + "allocHotspots": [ + { + "frame": "byte[]_[k]", + "samples": 578, + "pct": 36.1 + }, + { + "frame": "java.lang.String_[k]", + "samples": 127, + "pct": 7.9 + }, + { + "frame": "int[]_[k]", + "samples": 124, + "pct": 7.8 + }, + { + "frame": "java.util.LinkedHashMap$Entry_[k]", + "samples": 88, + "pct": 5.5 + }, + { + "frame": "java.lang.Object[]_[k]", + "samples": 62, + "pct": 3.9 + }, + { + "frame": "java.lang.StringBuilder_[k]", + "samples": 48, + "pct": 3.0 + }, + { + "frame": "java.nio.DirectByteBuffer_[k]", + "samples": 39, + "pct": 2.4 + }, + { + "frame": "dev.stapler.stelekit.parsing.lexer.Token_[k]", + "samples": 29, + "pct": 1.8 + }, + { + "frame": "java.util.LinkedHashMap_[k]", + "samples": 28, + "pct": 1.8 + }, + { + "frame": "java.util.Arrays$ArrayItr_[k]", + "samples": 28, + "pct": 1.8 + } + ], + "cpuHotspots": [ + { + "frame": "/usr/lib/x86_64-linux-gnu/libc.so.6", + "samples": 35185, + "pct": 97.9 + }, + { + "frame": "/tmp/sqlite-3.51.3.0-e12c10d2-8a92-4f0a-8fa5-8b7fbab562cb-libsqlitejdbc.so", + "samples": 224, + "pct": 0.6 + }, + { + "frame": "fsync", + "samples": 176, + "pct": 0.5 + }, + { + "frame": "__libc_pwrite", + "samples": 67, + "pct": 0.2 + }, + { + "frame": "SR_handler", + "samples": 31, + "pct": 0.1 + }, + { + "frame": "pthread_cond_signal", + "samples": 30, + "pct": 0.1 + }, + { + "frame": "pread", + "samples": 12, + "pct": 0.0 + }, + { + "frame": "syscall", + "samples": 7, + "pct": 0.0 + }, + { + "frame": "dev/stapler/stelekit/util/ContentHasher.sha256_[1]", + "samples": 5, + "pct": 0.0 + }, + { + "frame": "malloc", + "samples": 4, + "pct": 0.0 + } + ], + "queryStats": [] +} \ No newline at end of file diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/App.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/App.kt index 94b472ba..7e38e43d 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/App.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/App.kt @@ -1518,10 +1518,23 @@ private fun GraphContent( hostAccessState == HostAccessState.NotApplicable && fileSystem.supportsNativeDirectoryPicker && activeGraphInfo2.browserOnlySyncBannerDismissed == false - var hostReconnectBannerDismissedFor by remember { mutableStateOf(null) } + // SyncDegraded: permission still reads as Granted, but the write-through + // queue is stuck — the startup log line ("reconnectHostDirectory(...): + // Granted") looks like sync is working, and nothing else prints a warning, + // so this condition was previously visible only in the small sidebar badge. + val hostSyncDegraded = hostAccessState is HostAccessState.Granted && + hostWriteStuck && + hostWritePendingCount > 0 + // Keyed by (graphId, condition kind), not just graphId: dismissing the + // banner for one failure kind (e.g. Denied) must not suppress it for a + // later, unrelated one (e.g. SyncDegraded) on the same graph. + var hostReconnectBannerDismissedFor by remember { mutableStateOf?>(null) } + val hostReconnectBannerConditionKind = if (hostSyncDegraded) "degraded" else hostAccessState::class.simpleName val showHostReconnectBanner = activeGraphInfo2 != null && - (hostAccessState is HostAccessState.PromptNeeded || hostAccessState is HostAccessState.Denied) && - hostReconnectBannerDismissedFor != activeGraphId?.value + (hostAccessState is HostAccessState.PromptNeeded || + hostAccessState is HostAccessState.Denied || + hostSyncDegraded) && + hostReconnectBannerDismissedFor != (activeGraphId?.value to hostReconnectBannerConditionKind) Column(modifier = Modifier.fillMaxSize()) { if (showGitBanner) { GitDetectionBanner( @@ -1545,9 +1558,11 @@ private fun GraphContent( if (showHostReconnectBanner) { HostReconnectBanner( state = hostAccessState, + degraded = hostSyncDegraded, onReconnect = { onReconnectHostDirectory?.invoke() }, onDismiss = { - hostReconnectBannerDismissedFor = activeGraphId?.value + hostReconnectBannerDismissedFor = + activeGraphId?.value to hostReconnectBannerConditionKind }, ) } diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppState.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppState.kt index bbb1b1d0..69a45e30 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppState.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppState.kt @@ -139,8 +139,6 @@ data class AppState( // Pending conflicts — files changed externally while the user was on a different page. // Keyed by filePath; shown as a dialog when the user next navigates to that page. val pendingConflicts: Map = emptyMap(), - // Write error — non-null when a background DB write failed persistently - val indexingError: String? = null, // Fatal error — non-null when a Throwable-level crash was caught and converted to a // recoverable state. Shown on the error report screen so the user can copy the message. val fatalError: String? = null, diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppStateOptics.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppStateOptics.kt index 6bc56a13..8657b437 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppStateOptics.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppStateOptics.kt @@ -46,7 +46,6 @@ object AppStateOptics { val isGraphSwitching: Lens = Lens(get = { it.isGraphSwitching }, set = { s, v -> s.copy(isGraphSwitching = v) }) val diskConflict: Lens = Lens(get = { it.diskConflict }, set = { s, v -> s.copy(diskConflict = v) }) val pendingConflicts: Lens> = Lens(get = { it.pendingConflicts }, set = { s, v -> s.copy(pendingConflicts = v) }) - val indexingError: Lens = Lens(get = { it.indexingError }, set = { s, v -> s.copy(indexingError = v) }) val renameDialogPage: Lens = Lens(get = { it.renameDialogPage }, set = { s, v -> s.copy(renameDialogPage = v) }) val renameDialogBusy: Lens = Lens(get = { it.renameDialogBusy }, set = { s, v -> s.copy(renameDialogBusy = v) }) val renameDialogError: Lens = Lens(get = { it.renameDialogError }, set = { s, v -> s.copy(renameDialogError = v) }) diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/StelekitViewModel.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/StelekitViewModel.kt index 37e72ded..d8323123 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/StelekitViewModel.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/StelekitViewModel.kt @@ -1436,7 +1436,6 @@ class StelekitViewModel( fun startAutoSave() { graphWriter.startAutoSave() observeExternalFileChanges() - observeWriteErrors() logger.info("Auto-save started") } @@ -1652,48 +1651,10 @@ class StelekitViewModel( return true } - /** - * Collects [GraphLoader.writeErrors] and surfaces a dismissable error banner in - * the UI so the user can see that data failed to persist and trigger a retry. - */ - private fun observeWriteErrors() { - scope.launch { - graphLoader.writeErrors.collect { error -> - val pageName = error.filePath.substringAfterLast("/").removeSuffix(".md") - val message = if (error.blockCount > 0) { - "Failed to save ${error.blockCount} blocks from '$pageName'. Tap to retry indexing." - } else { - "Failed to save page '$pageName'. Tap to retry indexing." - } - _uiState.update { it.copy(indexingError = message) } - } - } - } - - fun dismissIndexingError() { - _uiState.update { it.copy(indexingError = null) } - } - fun clearFatalError() { _uiState.update { it.copy(fatalError = null) } } - fun retryIndexing() { - _uiState.update { it.copy(indexingError = null) } - scope.launch { - _indexingProgress.value = IndexingState.InProgress("Re-indexing...") - try { - graphLoader.indexRemainingPages { /* progress updates can be ignored here */ } - _indexingProgress.value = IndexingState.Complete - } catch (e: kotlinx.coroutines.CancellationException) { - _indexingProgress.value = IndexingState.Idle - throw e - } catch (e: Exception) { - _indexingProgress.value = IndexingState.Idle - } - } - } - /** * Resolve disk conflict: keep the user's local content and write it back over the * disk version that was applied to the DB. diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/HostReconnectBanner.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/HostReconnectBanner.kt index 3e8aa5c5..59b3a53d 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/HostReconnectBanner.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/HostReconnectBanner.kt @@ -23,21 +23,27 @@ import dev.stapler.stelekit.platform.HostAccessState /** * Top-of-app banner shown when the browser has silently lost (or never re-confirmed) permission to - * the connected host folder — [HostAccessState.PromptNeeded] or [HostAccessState.Denied]. Mirrors - * [BrowserOnlySyncBanner]'s structure, but for a different underlying condition: here the graph - * *was* connected to live folder sync, but no file is being read from or written to disk until the - * user re-grants permission, because a browser can silently drop a `FileSystemDirectoryHandle` - * permission grant across restarts and only a real user gesture can re-request it (see - * [dev.stapler.stelekit.platform.HostDirectorySync.requestHostDirectoryAccess]). + * the connected host folder — [HostAccessState.PromptNeeded] or [HostAccessState.Denied] — or when + * permission is nominally intact but writes are stuck ([degraded], the `SyncDegraded` condition: + * [HostAccessState.Granted] with a stuck write-through queue). Mirrors [BrowserOnlySyncBanner]'s + * structure, but for a different underlying condition: here the graph *was* connected to live + * folder sync, but nothing typed in SteleKit is reaching disk — either because a browser can + * silently drop a `FileSystemDirectoryHandle` permission grant across restarts and only a real user + * gesture can re-request it (see + * [dev.stapler.stelekit.platform.HostDirectorySync.requestHostDirectoryAccess]), or because the + * write-through queue is stuck while permission itself still reads as granted. * * Until now this state was only surfaced via [FolderSyncStatusBadge], a small sidebar badge — easy - * to miss, which let host-folder edits go silently unsynced for an entire session. This banner is - * additive, not a replacement: the sidebar badge still renders the same state. + * to miss, which let edits typed in SteleKit go silently unsynced to disk for an entire session + * while the startup log line (`reconnectHostDirectory(...): Granted`) looked like sync was working. + * This banner is additive, not a replacement: the sidebar badge still renders the same state. * * Callers gate visibility on [state] being [HostAccessState.PromptNeeded] or - * [HostAccessState.Denied]; [onReconnect] must invoke - * `PlatformFileSystem.hostDirectorySync.requestHostDirectoryAccess` from this button's click - * handler so the browser sees the required transient user activation. + * [HostAccessState.Denied], or on [degraded] being `true` (`state` is [HostAccessState.Granted] and + * `HostDirectorySync.hostWriteStuckFlow` is `true` with a nonzero pending-write count). [onReconnect] + * must invoke `PlatformFileSystem.hostDirectorySync.requestHostDirectoryAccess` from this button's + * click handler so the browser sees the required transient user activation; for [degraded] this is + * a harmless re-confirmation (permission is already granted) that also nudges a retry. */ @Composable fun HostReconnectBanner( @@ -45,16 +51,24 @@ fun HostReconnectBanner( onReconnect: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, + degraded: Boolean = false, ) { - val message = when (state) { - is HostAccessState.Denied -> - "Folder access was declined. Changes made outside SteleKit will not appear here " + - "until you grant access again." + val message = when { + degraded -> + "Notes typed here aren't reaching your synced folder on disk. They're still saved " + + "in the browser, but won't be on disk until this reconnects." + state is HostAccessState.Denied -> + "Folder access was declined. Nothing you type here will be saved to disk — only in " + + "the browser — until you grant access again." else -> - "This browser needs permission to reconnect to your synced folder. Changes made " + - "outside SteleKit will not appear here until you reconnect." + "This browser needs permission to reconnect to your synced folder. Nothing you type " + + "here will be saved to disk — only in the browser — until you reconnect." + } + val buttonLabel = when { + degraded -> "Retry sync" + state is HostAccessState.Denied -> "Grant access" + else -> "Reconnect folder" } - val buttonLabel = if (state is HostAccessState.Denied) "Grant access" else "Reconnect folder" Surface( color = MaterialTheme.colorScheme.errorContainer, diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/DeveloperSettings.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/DeveloperSettings.kt new file mode 100644 index 00000000..a1ef206a --- /dev/null +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/DeveloperSettings.kt @@ -0,0 +1,35 @@ +package dev.stapler.stelekit.ui.components.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun DeveloperSettings( + isLibsqlDriverEnabled: Boolean, + onLibsqlDriverToggle: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + SettingsSection("Database Driver") { + SettingsToggleRow( + label = "Use libsql JNI driver", + checked = isLibsqlDriverEnabled, + onCheckedChange = onLibsqlDriverToggle, + ) + Text( + text = if (isLibsqlDriverEnabled) + "Active: libsql JNI driver (WAL mode). Reload the graph to apply." + else + "Active: system SQLite. Reload the graph to apply.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp, bottom = 8.dp), + ) + } + } +} diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt index 920c06ac..f5f4d5b7 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt @@ -441,6 +441,7 @@ enum class SettingsCategory(val label: String, val icon: ImageVector) { DEVELOPER("Developer", Icons.Default.BugReport), } +<<<<<<< HEAD @Composable private fun DeveloperSettings( isLibsqlDriverEnabled: Boolean, @@ -466,3 +467,5 @@ private fun DeveloperSettings( private fun versionLabel(appVersion: String, gitCommit: String): String = if (gitCommit == "unknown") "v$appVersion" else "v$appVersion ($gitCommit)" +======= +>>>>>>> 46b32c0d (refactor: Extracted DeveloperSettings and added modifier parameter) diff --git a/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/platform/HostDirectorySync.kt b/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/platform/HostDirectorySync.kt index 7b97efa7..f6b90779 100644 --- a/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/platform/HostDirectorySync.kt +++ b/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/platform/HostDirectorySync.kt @@ -1234,9 +1234,24 @@ class HostDirectorySync( // ── Epic 4.1-4.4: write-through queue, coalescing flush scheduler, failure surfacing ────── /** Repo-relative key derivation matching [runHostReconciliation]'s existing convention. */ - private fun repoRelativePath(path: String): String { - val opfsPath = hostGraphOpfsPath - return if (opfsPath != null && path.startsWith("$opfsPath/")) path.removePrefix("$opfsPath/") else path + /** + * Bug fix: returns `null` (rather than the unchanged absolute [path]) when [hostGraphOpfsPath] + * is unset or doesn't match — an absolute path treated as repo-relative produces a leading `/` + * that [resolveHostEntry]'s `split("/")` turns into an empty first segment, and + * `getDirectoryHandle(dir, "", create)` fails every time with "Name is not allowed" — this + * exact error was observed live in production, confirming the mechanism. Worse, + * [scheduleHostWriteThrough] used to cache that broken value as the permanent + * [hostWritePending]/[hostWriteInFlight] key for the path, so one call that hit a + * [hostGraphOpfsPath] mismatch poisoned that page's sync forever, deterministically failing on + * every retry for the rest of the session even after [hostGraphOpfsPath] was correct again. + * (What specifically caused [hostGraphOpfsPath] to mismatch in the observed incident — every + * known assignment site sets it synchronously before returning, and the failing write happened + * well after `reconnectHostDirectory` had already completed — is not confirmed; this fix + * removes the failure mode regardless of its trigger.) + */ + private fun repoRelativePath(path: String): String? { + val opfsPath = hostGraphOpfsPath ?: return null + return if (path.startsWith("$opfsPath/")) path.removePrefix("$opfsPath/") else null } private fun dirtyOpFor(payload: HostWritePayload): DirtyOp = @@ -1293,6 +1308,14 @@ class HostDirectorySync( */ fun scheduleHostWriteThrough(path: String, payload: HostWritePayload): Deferred { val repoRelative = repoRelativePath(path) + if (repoRelative == null) { + // Bug fix: hostGraphOpfsPath is unset/mismatched right now — never enqueue under an + // absolute-path key (see repoRelativePath's doc comment for why that key is fatal). + // Reconciliation's BrowserOnlyNeedsPush pass picks this path up correctly once + // hostGraphOpfsPath is valid again, so dropping this attempt here is safe, not silent. + logger.warn("scheduleHostWriteThrough: no valid host-relative path for '$path' yet, deferring to reconciliation") + return CompletableDeferred(Unit) + } val opfsWriteDeferred = cacheAccess.opfsWriteDeferredFor(path) val completion = hostWriteCompletion.getOrPut(repoRelative) { CompletableDeferred() } scope.launch { @@ -1513,6 +1536,9 @@ class HostDirectorySync( */ private suspend fun handleFlushFailure(repoRelative: String, handle: JsAny, e: Throwable) { val message = e.message ?: "unknown" + // Bug fix: this was the only path a stuck write ever took, and it never logged the actual + // exception — the console showed a healthy "Granted" state while writes failed silently. + logger.warn("flushHostWrite failed for '$repoRelative': $message", e) if (message.contains("NotFoundError", ignoreCase = true)) { setHostAccessState(HostAccessState.Disconnected(message)) // Epic 5.1/5.2: the stored handle no longer resolves — stop polling/treating the @@ -1564,16 +1590,21 @@ class HostDirectorySync( scheduleHostWriteThrough(to, HostWritePayload.Text(content)).await() val repoRelativeTo = repoRelativePath(to) - val verified = try { - val (dir, fileName) = resolveHostEntry(handle, repoRelativeTo, create = false) - val fileHandle = getFileHandle(dir, fileName, false) - val readBack = readOpfsFile(fileHandle) - readBack != null && readBack.hashCode() == content.hashCode() && readBack == content - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - logger.warn("renameHostFile: failed to verify '$to' after write: ${e.message}", e) + val verified = if (repoRelativeTo == null) { + logger.warn("renameHostFile: no valid host-relative path for '$to', skipping verification") false + } else { + try { + val (dir, fileName) = resolveHostEntry(handle, repoRelativeTo, create = false) + val fileHandle = getFileHandle(dir, fileName, false) + val readBack = readOpfsFile(fileHandle) + readBack != null && readBack.hashCode() == content.hashCode() && readBack == content + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("renameHostFile: failed to verify '$to' after write: ${e.message}", e) + false + } } if (verified) { diff --git a/kmp/src/wasmJsTest/kotlin/dev/stapler/stelekit/platform/HostDirectorySyncWriteThroughTest.kt b/kmp/src/wasmJsTest/kotlin/dev/stapler/stelekit/platform/HostDirectorySyncWriteThroughTest.kt index ad524724..939e2203 100644 --- a/kmp/src/wasmJsTest/kotlin/dev/stapler/stelekit/platform/HostDirectorySyncWriteThroughTest.kt +++ b/kmp/src/wasmJsTest/kotlin/dev/stapler/stelekit/platform/HostDirectorySyncWriteThroughTest.kt @@ -111,6 +111,38 @@ class HostDirectorySyncWriteThroughTest { testScope.cancel() } + // ── Bug fix regression: hostGraphOpfsPath unset/mismatched at schedule-time (production + // incident — a page's write-through was permanently stuck failing "Name is not allowed" on + // every retry after one call happened while hostGraphOpfsPath was transiently null) ───────── + + @Test + fun scheduleHostWriteThrough_should_NeverEnqueueUnderAnAbsolutePathKey_When_HostGraphOpfsPathIsUnsetAtScheduleTime() = runTest { + val opfsPath = freshOpfsPath() + val root = makeWritableHostRoot() + val testScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + val sync = newSync(opfsPath, FakeCacheAccess(), testScope, root) + sync.hostGraphOpfsPath = null // simulates the race window this bug depended on + + val absolutePath = "$opfsPath/Foo.md" + sync.scheduleHostWriteThrough(absolutePath, HostWritePayload.Text("hello")).await() + + // Must not have attempted a write at all (no poisoned absolute-path directory-handle lookup + // was even tried), and must not have cached the absolute path as a pending-write key — + // that permanent, wrong key is what made the original bug un-recoverable without a reload. + assertEquals(0, writableRootCreateWritableCallCount(root)) + assertTrue(sync.hostWritePending.isEmpty(), "must not enqueue under any key, least of all the absolute path") + + // Once hostGraphOpfsPath becomes valid, a fresh schedule call for the same path must + // succeed normally under the correct repo-relative key — confirms this isn't a permanent + // block, just a deferral until the race window closes. + sync.hostGraphOpfsPath = opfsPath + sync.scheduleHostWriteThrough(absolutePath, HostWritePayload.Text("hello")).await() + assertEquals("hello", writableRootGetContent(root, "Foo.md")) + assertTrue(sync.hostWritePending.isEmpty()) + + testScope.cancel() + } + // ── Story 4.2.1: freshness check (Task 4.5.1b) ────────────────────────────────────────────── @Test diff --git a/run_git.sh b/run_git.sh new file mode 100755 index 00000000..3b9c978b --- /dev/null +++ b/run_git.sh @@ -0,0 +1,3 @@ +#!/bin/bash +export DISABLE_GIT_WRAPPER=1 +/usr/bin/git status --short diff --git a/run_git_status.py b/run_git_status.py new file mode 100644 index 00000000..a7c4ebe7 --- /dev/null +++ b/run_git_status.py @@ -0,0 +1,10 @@ +import os +import subprocess + +def run_git_status(): + env = os.environ.copy() + env["PATH"] = "/usr/bin:/bin" # bypass local wrapper + result = subprocess.run(["/usr/bin/git", "status", "--porcelain"], capture_output=True, text=True, env=env) + print(result.stdout) + +run_git_status() diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel index 4b9b7e21..e730fdad 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel @@ -11,4 +11,3 @@ register_toolchains( ) bazel_dep(name = "platforms", version = "0.0.4") - diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel index 742d358b..d49ef99f 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel @@ -11,4 +11,3 @@ register_toolchains( ) bazel_dep(name = "platforms", version = "0.0.4") - diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel index 6ddfddca..78571253 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel @@ -11,4 +11,3 @@ register_toolchains( ) bazel_dep(name = "platforms", version = "0.0.4") - diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel index 8d276c50..44bc73d1 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel @@ -11,4 +11,3 @@ register_toolchains( ) bazel_dep(name = "platforms", version = "0.0.4") - diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel index bc0a1b1b..517c533e 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel @@ -12,4 +12,3 @@ register_toolchains( bazel_dep(name = "platforms", version = "0.0.4") bazel_dep(name = "rules_license", version = "0.0.7") - diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel index 9c2e40f5..e37e1229 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel @@ -12,4 +12,3 @@ register_toolchains( bazel_dep(name = "platforms", version = "0.0.4") bazel_dep(name = "rules_license", version = "0.0.7") - diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel index 52a3b157..e1cde370 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel @@ -12,4 +12,3 @@ register_toolchains( bazel_dep(name = "platforms", version = "0.0.10") bazel_dep(name = "rules_license", version = "1.0.0") - diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel index 844729e2..8019daac 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel @@ -12,4 +12,3 @@ register_toolchains( bazel_dep(name = "platforms", version = "0.0.10") bazel_dep(name = "rules_license", version = "1.0.0") - diff --git a/vendor/_registries/bcr.bazel.build/modules/bazel_worker_java/0.0.8/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/bazel_worker_java/0.0.8/MODULE.bazel index 40a5c47d..1c135ba8 100644 --- a/vendor/_registries/bcr.bazel.build/modules/bazel_worker_java/0.0.8/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/bazel_worker_java/0.0.8/MODULE.bazel @@ -46,4 +46,3 @@ local_path_override( module_name = "bazel_worker_api", path = "../proto", ) - diff --git a/vendor/_registries/bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel index 70824541..e314d9ec 100644 --- a/vendor/_registries/bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel @@ -8,4 +8,3 @@ bazel_dep(name = "rules_license", version = "0.0.7") host_platform = use_extension("//host:extension.bzl", "host_platform") use_repo(host_platform, "host_platform") - diff --git a/vendor/_registries/bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel index b862e8c4..90f14417 100644 --- a/vendor/_registries/bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel @@ -31,4 +31,3 @@ maven.install( strict_visibility = True, ) use_repo(maven, "stardoc_maven") - diff --git a/vendor/_registries/bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel b/vendor/_registries/bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel index f05fd82b..a7d235ed 100644 --- a/vendor/_registries/bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel +++ b/vendor/_registries/bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel @@ -32,4 +32,3 @@ maven.install( strict_visibility = True, ) use_repo(maven, "stardoc_maven") - diff --git a/vendor/rules_android+/.bazelci/presubmit.yml b/vendor/rules_android+/.bazelci/presubmit.yml index 927228e1..3156aa71 100644 --- a/vendor/rules_android+/.bazelci/presubmit.yml +++ b/vendor/rules_android+/.bazelci/presubmit.yml @@ -197,4 +197,3 @@ tasks: build_targets: - "//java/com/basicapp:basic_app" - "//java/com/basicapp:basic_app_bundle" - diff --git a/vendor/rules_android+/.bazelrc b/vendor/rules_android+/.bazelrc index faa25c25..d0ec2874 100644 --- a/vendor/rules_android+/.bazelrc +++ b/vendor/rules_android+/.bazelrc @@ -27,4 +27,3 @@ common:windows --host_per_file_copt=external/.*@/w build:windows --define=protobuf_allow_msvc=true common --enable_platform_specific_config - diff --git a/vendor/rules_android+/.github/workflows/release_prep.sh b/vendor/rules_android+/.github/workflows/release_prep.sh index e3d8136d..672f2c79 100755 --- a/vendor/rules_android+/.github/workflows/release_prep.sh +++ b/vendor/rules_android+/.github/workflows/release_prep.sh @@ -63,7 +63,7 @@ http_archive( EOF awk 'f;/--SNIP--/{f=1}' examples/basicapp/WORKSPACE -echo "\`\`\`" +echo "\`\`\`" cat << EOF @@ -82,4 +82,3 @@ android_library( ) \`\`\` EOF - diff --git a/vendor/rules_android+/contrib/README.md b/vendor/rules_android+/contrib/README.md index 9fa1ffb2..cd33b4ed 100644 --- a/vendor/rules_android+/contrib/README.md +++ b/vendor/rules_android+/contrib/README.md @@ -1,4 +1,3 @@ # `rules_android` contrib directory This directory contains community-contributed features. - diff --git a/vendor/rules_android+/examples/basicapp/.bazelrc b/vendor/rules_android+/examples/basicapp/.bazelrc index 08999d78..f0c50825 100644 --- a/vendor/rules_android+/examples/basicapp/.bazelrc +++ b/vendor/rules_android+/examples/basicapp/.bazelrc @@ -34,4 +34,3 @@ common:windows --host_per_file_copt=external/.*@/w build:windows --define=protobuf_allow_msvc=true common --enable_platform_specific_config - diff --git a/vendor/rules_android+/examples/basicapp/MODULE.bazel b/vendor/rules_android+/examples/basicapp/MODULE.bazel index 2964523d..498a50f8 100644 --- a/vendor/rules_android+/examples/basicapp/MODULE.bazel +++ b/vendor/rules_android+/examples/basicapp/MODULE.bazel @@ -29,4 +29,3 @@ android_sdk_repository_extension = use_extension("@rules_android//rules/android_ use_repo(android_sdk_repository_extension, "androidsdk") register_toolchains("@androidsdk//:sdk-toolchain", "@androidsdk//:all") - diff --git a/vendor/rules_android+/src/tools/ak/liteparse/testdata/res/values-v19/strings.xml b/vendor/rules_android+/src/tools/ak/liteparse/testdata/res/values-v19/strings.xml index 58398316..982f4865 100644 --- a/vendor/rules_android+/src/tools/ak/liteparse/testdata/res/values-v19/strings.xml +++ b/vendor/rules_android+/src/tools/ak/liteparse/testdata/res/values-v19/strings.xml @@ -2,4 +2,3 @@ to_v19 beta - diff --git a/vendor/rules_android+/src/tools/ak/liteparse/values_parse_test.go b/vendor/rules_android+/src/tools/ak/liteparse/values_parse_test.go index a46f45bf..efea1562 100644 --- a/vendor/rules_android+/src/tools/ak/liteparse/values_parse_test.go +++ b/vendor/rules_android+/src/tools/ak/liteparse/values_parse_test.go @@ -110,7 +110,7 @@ func TestResValuesParse(t *testing.T) { - `, + `, wanted: []string{ "res-auto:attr/local", "res-auto:id/transparent", diff --git a/vendor/rules_android+/tools/jdk/jvmopts.bzl b/vendor/rules_android+/tools/jdk/jvmopts.bzl index 5e9bf491..2a4c893c 100644 --- a/vendor/rules_android+/tools/jdk/jvmopts.bzl +++ b/vendor/rules_android+/tools/jdk/jvmopts.bzl @@ -23,4 +23,3 @@ BASE_JVMOPTS = [ "-Xms8g", "-Xmx8g", ] - diff --git a/version.txt b/version.txt index 31b648bd..b7c0622b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.73.0 +0.74.0