Skip to content

chore(main): release 0.9.5 - #30

Merged
tstapler merged 1 commit into
mainfrom
release-please--branches--main
Apr 25, 2026
Merged

tstapler merged 1 commit into
mainfrom
release-please--branches--main

Conversation

@github-actions

@github-actions github-actions Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

🤖 I have created a release beep boop

0.9.5 (2026-04-25)

Bug Fixes

  • resilience: fix loading screen hang + isolate test settings (dc1b51b)

Performance Improvements

  • android: decompose Phase 3 chunk writes to allow HIGH-priority preemption (#26) (b5a51ab)

This PR was generated with Release Please. See documentation.

@github-actions
github-actions Bot force-pushed the release-please--branches--main branch from d9aab1b to 3ab5352 Compare April 24, 2026 18:05
@github-actions
github-actions Bot force-pushed the release-please--branches--main branch from 3ab5352 to bf44fcb Compare April 25, 2026 04:24
@tstapler
tstapler merged commit 283753a into main Apr 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor Author

🤖 Created releases:

🌻

tstapler added a commit that referenced this pull request Apr 25, 2026
Brings in:
- perf(android): decompose Phase 3 chunk writes (b5a51ab)
- chore(main): release 0.9.5 (#30)

Conflict resolution:
- build.gradle.kts: keep minSdk=26 (required by genai-prompt:1.0.0-beta2)
  and take testInstrumentationRunner from main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tstapler pushed a commit that referenced this pull request Apr 26, 2026
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
tstapler added a commit that referenced this pull request Apr 26, 2026
…r icons (#42)

* feat(fdroid): fix repo version history and show version in Settings

- F-Droid workflow now downloads APKs from all releases instead of only
  the latest, so the repo contains full version history and upgrades
  work correctly without needing to uninstall/reinstall.
- DeviceInfo.android.kt reads actual versionName from PackageManager
  instead of the hardcoded "1.0.0".
- SettingsDialog shows the app version (e.g. "v0.8.1") at the bottom
  of the settings sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Copilot review — iOS version and fdroid error handling

- DeviceInfo.ios.kt: read CFBundleShortVersionString from NSBundle
  instead of returning hardcoded "1.0.0", so the Settings version label
  is accurate on iOS.
- fdroid.yml: replace blanket `|| true` with explicit "no assets match"
  guard; fail fast on auth/network/ratelimit errors and emit a clear
  error if zero APKs are downloaded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(bench): benchmark summary a9770de

* fix(sync): prevent data-loss races on mobile reload and external conflict handling (#21)

* fix(sync): prevent data-loss races on mobile reload and external conflict handling

- Android: wire up SafChangeDetector ContentObserver in GraphLoader.startWatching();
  add SAF-optimized listFilesWithModTimes to avoid N+1 queries per poll cycle
- iOS: fix getLastModifiedTime (was hardcoded null), listFiles (empty list bug),
  listDirectories and directoryExists (didn't distinguish files from dirs)
- DebounceManager: add cancel(key) and hasPending(key) suspend fns for targeted
  per-key cancellation and pending-state inspection
- FileRegistry.detectChanges: wrap in Mutex to prevent double-emit on concurrent
  watcher poll + ContentObserver callback races
- BlockStateManager: add hasPendingDiskWrite / cancelPendingDiskSave using the
  new DebounceManager APIs
- StelekitViewModel: extend shouldProtect to cover the DB-saved-but-not-yet-written
  window (~300ms); cancel pending disk save when conflict dialog is shown;
  acceptDiskVersion flushes to disk via savePageNow to maintain consistency
- Tests: comprehensive coverage for all five race-condition fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(state): unobservePage now clears blocks from state map

The previous implementation cancelled the coroutine job but left the
page's blocks in _blocks indefinitely, causing unbounded memory growth
as users navigate pages. Now evicts the page entry on unobserve, which
also aligns with the unobservePage_clears_state test contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(sync): coalesce ContentObserver callbacks with Channel.CONFLATED

Rapid ContentObserver signals (or the SAF 30s polling fallback) could
queue up many redundant directory scans — each callback launched an
independent parallelScope coroutine. Replace with a Channel(CONFLATED)
that drops all-but-one pending trigger, so at most one extra scan is
queued during a burst. The consumer coroutine is now a child of
watcherJob so it is properly cancelled when startWatching is called
again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ios): fix commonMain compilation errors in PerfExporter and PerformanceDashboard

String.format() and Dispatchers.IO are JVM-only; replace with KMP-compatible
padStart string building and PlatformDispatcher.IO respectively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(bench): benchmark summary 105928d

* feat(search): AND semantics, field boosting, recency + graph distance ranking (#22)

- FtsQueryBuilder: multi-term queries now use AND (all terms required) with
  wildcard on every token; OR fallback fires automatically when AND returns empty
- BM25 scores exposed on SearchedPage/SearchedBlock; RankedSearchHit sealed class
  added for cross-type ranking with abstract score field
- PAGE_BOOST (5×): page-title FTS hits ranked above body-text hits
- RECENCY_HALFLIFE_DAYS (14d): exponential decay gives recently-edited results
  up to 2× score multiplier
- GRAPH_BOOST (3×): results from 1-hop neighbour pages (via block_references)
  receive a 3× multiplier; selectNeighbourPageUuids UNION query pre-fetches
  the full neighbour set in a single indexed read
- 37 tests covering AND semantics, OR fallback, field boost, graph distance, recency

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(main): release 0.9.0 (#20)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(bench): benchmark summary 73cc4e8

* chore: update Homebrew formula for v0.9.0

* fix(cache): work around Kotlin 2.3.10 K2 compiler bug in LruCache

When all public members of a class are suspend fun, the K2 compiler
silently omits the outer .class file for the JVM target while still
generating the inner coroutine continuation classes. This caused a
NoClassDefFoundError: dev/stapler/stelekit/cache/LruCache at runtime.

Three workarounds applied:
- Extract weigher to fun interface LruWeigher<K,V> (SAM-convertible,
  call sites unchanged — lambdas still work via SAM conversion)
- Replace Mutex.withLock{} (inline suspend) with explicit lock()/unlock()
- Add val capacity: Long (non-suspend anchor that forces outer class emission)

The root trigger is the all-suspend public API. Adding any one non-suspend
public member to the class is sufficient to fix the missing .class file.

Bug filed: https://youtrack.jetbrains.com/issue/KT-XXXXX

* chore(bench): benchmark summary ba30932

* chore(main): release 0.9.1 (#23)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(bench): benchmark summary 921e56f

* chore: update Homebrew formula for v0.9.1

* fix(cache): strengthen LruCache.class workaround for Linux CI

The previous workaround (val capacity: Long) forced LruCache.class
emission on macOS but the Kotlin 2.3.10 K2 compiler still dropped the
class on Linux. Adding override fun toString() as a second non-suspend
anchor reliably triggers outer class emission on both platforms.

* chore(bench): benchmark summary 59ae017

* chore(main): release 0.9.2 (#24)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(bench): benchmark summary 6387e79

* chore: update Homebrew formula for v0.9.2

* fix(cache): replace coroutine Mutex with synchronized in LruCache

The Kotlin 2.3.10 K2 compiler silently drops LruCache.class on Linux
when all public members are suspend fun — non-suspend anchors (val
capacity, override toString) fixed the issue locally on macOS but not
in the Linux CI build environment.

Switch to synchronized(this) instead of Mutex, making all methods
plain fun. Operations are O(1) and hold the lock for microseconds so
brief thread blocking is acceptable. Callers need no changes since
non-suspend functions are callable from suspend contexts.

The suspend → fun change also eliminates the all-suspend-public-API
compiler bug entirely: no coroutine inner classes are generated so
LruCache.class is always emitted.

* chore(bench): benchmark summary 17345e4

* chore(main): release 0.9.3 (#25)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(bench): benchmark summary 8121f76

* chore: update Homebrew formula for v0.9.3

* fix(cache): rename LruCache → SteleLruCache to bypass K2 compiler bug

Kotlin 2.3.10 K2 on the macOS CI runner silently drops the .class file
for a class named exactly "LruCache" in this package. All other workarounds
(val property anchor, toString override, synchronized instead of suspend)
failed to prevent the omission.

Renaming to SteleLruCache produces a valid .class file. Type aliases
LruCache and LruWeigher are kept so all call sites require zero changes.

* chore(bench): benchmark summary ea70c86

* ci(macos): add gradle-home-cache-strict-match to prevent stale IC cache restore

Prevents the Gradle Actions setup from falling back to a prefix-match
cache restore for the macOS desktop build. Without this, stale Kotlin
Incremental Compilation state in ~/.gradle/caches can cause the K2
compiler to reuse old output and silently omit .class files for renamed
or modified classes.

This pairs with the SteleLruCache rename (v0.9.4) to ensure a clean
compile on macOS CI.

* chore(bench): benchmark summary ba0aed2

* ci: unify Gradle cache configuration across all build jobs

All four build jobs (android, linux, macos, windows) now share identical
setup-gradle options:
  - gradle-home-cache-cleanup: true
  - gradle-home-cache-strict-match: true  (was macOS-only)
  - cache-encryption-key

The strict-match flag prevents the Gradle Actions prefix-match fallback
from restoring stale Kotlin Incremental Compilation state from a
different commit's cache entry. Without it, the K2 compiler can reuse
old IC output and silently drop .class files for renamed or modified
classes.

The macOS build command is also aligned with other desktop jobs:
  - Removed :kmp:clean (was a workaround; strict-match is the real fix)
  - Added --build-cache (now consistent with linux and windows)

* chore(bench): benchmark summary d8346ce

* ci: add smoke-test startup steps to all three desktop build jobs

Each desktop job now launches the final packaged artifact and waits 20
seconds to confirm the app stays alive. A JVM crash (e.g. from a
NoClassDefFoundError) exits within ~2 seconds and fails the job before
the artifact is uploaded.

- Linux:   Xvfb virtual display + AppImage launch
- macOS:   mount DMG, strip quarantine, run binary from mount point
- Windows: createDistributable + PowerShell Start-Process check

Windows also adds :kmp:createDistributable so the smoke test has a
binary to run (the MSI installer cannot be tested without installing).
The rename step is updated to exclude app/ subdirs when searching for
the installer artifact.

* chore(bench): benchmark summary d9ff7b3

* chore(main): release 0.9.4 (#28)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(bench): benchmark summary f2d6623

* chore: update Homebrew formula for v0.9.4

* fix(resilience): fix loading screen hang + isolate test settings

Three root causes of the infinite loading spinner, all fixed:

1. App.kt: use try/finally around migrationReady=false so it always
   resets to true even if the LaunchedEffect is cancelled mid-run.

2. App.kt: initialize currentGraphPath from the persisted active graph
   (graphManager.getActiveGraphInfo()?.path) instead of the hardcoded
   getDefaultGraphPath() which always pointed to ~/Documents/stelekit,
   causing an unnecessary graph-switch on every launch.

3. StelekitViewModel: catch CancellationException separately before the
   generic Exception catch and rethrow it — prevents structured-concurrency
   cancellation from being swallowed, which left isLoading stuck at true.

Test isolation (the source of /tmp/rp-stale in production prefs):

4. Extract Settings interface from PlatformSettings expect class so tests
   can pass InMemorySettings instead of the file-backed PlatformSettings.
   All test files (RecentPagesTest, DiskConflictResolutionTest,
   ComposeUITestBase, TopBarTest, PageViewUITest, DesktopScreenshotTest)
   now use InMemorySettings — no more test paths leaking into
   ~/.stelekit/prefs.properties.

* chore(bench): benchmark summary dc1b51b

* test(ci): add KMP JVM test job + loading state regression tests

- Add test-kmp job to release.yml gating resolve-version on test pass
- Add StelekitViewModelLoadingTest: 6 pure-Kotlin tests covering isLoading
  state transitions, missing-directory error path, onboarding reset, scope
  cancellation resilience (CancellationException regression)
- Add MigrationReadyLoadingTest: 4 Compose tests for the try/finally
  migrationReady pattern that caused the infinite spinner
- Add waitForViewModelReady() extension to ComposeUITestBase

These tests would have caught the two bugs fixed in dc1b51b.

* perf(android): decompose Phase 3 chunk writes to allow HIGH-priority preemption (#26)

* perf(android): decompose Phase 3 chunk writes to allow HIGH-priority preemption

Phase 3 background indexing dispatched all saves for a 10-page chunk inside a
single Execute(LOW) lambda, holding DatabaseWriteActor for the full chunk duration.
A real Android session export showed a 4904ms db.queue_wait span on a HIGH-priority
request — user navigation was blocked for ~5s while indexing ran.

Replace flushChunkWrites (Execute-wrapped) with flushChunkWritesPreemptible, which
dispatches savePages, deleteBlocksForPages, and saveBlocks as separate typed LOW
actor calls. The actor can now service HIGH-priority SavePage/SaveBlocks requests
between each step, including between consecutive pages during block saves (via the
existing coalescing preemption in processSaveBlocks).

Also extend PerfExporter.export() to accept an optional directory parameter and
expose defaultExportDirectory(), wired up in PerformanceDashboard as an AlertDialog
that lets the user choose the export location before writing the JSON report.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(research): android save/parse performance investigation artifacts

MDD Phase 1–2 artifacts for the android-save-parse-performance project:
requirements, research plan, findings across 4 dimensions (stack, features,
architecture, pitfalls), and ADR-ready synthesis identifying DatabaseWriteActor
contention as the confirmed root cause (4904ms db.queue_wait span on Android).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(android): add instrumented benchmark to gate actor-contention regressions

Adds an androidInstrumentedTest in :kmp that runs on a real Android emulator
in CI via reactivecircus/android-emulator-runner (API 30, x86_64).

Two tests:
- loadPhaseTimings: measures phase 1 TTI + phase 3 indexRemainingPages duration,
  asserts phase 1 stays under 10s (conservative for emulator variance)
- writeLatencyDuringPhase3: the regression guard — concurrently runs indexRemainingPages
  and block saves every 200ms, asserts write p95 stays under 5s. Before the actor
  decomposition fix, this would have measured ~5000ms (Execute(LOW) holding the actor
  for a full 10-page chunk); after the fix it should be well under 2s.

Uses a lightweight inline FileSystem instead of PlatformFileSystem to avoid SAF
ContentProvider dependency in the test process. Generates 20 pages + 5 journals
directly into context.cacheDir so no SAF permissions are needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(bench): android benchmark history tracking and PR diff comments

Mirrors the JVM benchmark diff pattern for Android:
- AndroidGraphBenchmark now logs structured JSON to logcat (tag ANDROID_BENCH)
  at the end of each test: loadPhase and writeLatency metrics
- android-benchmark.yml captures logcat after the emulator run, parses the
  JSON lines with Python, and writes a timestamped summary to
  benchmarks/android-history/{slug}_{sha}.json
- On push to main: commits the new history file (same pattern as benchmark.yml)
- On PR: compares against the most recent baseline in android-history/ and
  posts (or updates) a comment with a delta table covering phase 1 TTI,
  phase 3 index time, write p95 baseline, write p95 during phase 3, and jank factor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(bench): remove backslash line continuations in emulator runner script

reactivecircus/android-emulator-runner executes each script line separately,
so backslash continuations are not shell-interpreted. Put the gradle command
on a single line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Copilot review comments

PerfExporter: treat blank directory string as absent so clearing the
path field in the export dialog falls back to the Downloads folder instead
of producing an invalid path like /stelekit-perf-....json.

GraphLoader: restore per-page atomicity in flushChunkWritesPreemptible.
The previous implementation called deleteBlocksForPages for all pages at
once then saveBlocks per page via separate typed requests. A HIGH write
could interleave between the delete and save for the same page, leaving
it transiently with no blocks. Fix: wrap each page's deleteBlocksForPages
+ saveBlocks inside a single Execute(LOW) so HIGH can only preempt between
pages (not within a page's delete+save sequence).

GraphLoader: move KDoc above @OptIn so it is attached to the function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve compile errors introduced by merging main

- Add `: Settings` to Android actual PlatformSettings (expect/actual mismatch
  from Settings interface added to main after our branch diverged)
- Make FakeFileSystem open so MissingDirectoryFileSystem in
  StelekitViewModelLoadingTest can extend it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove unimportable assertDoesNotExist import, add override modifiers

- Remove `import androidx.compose.ui.test.assertDoesNotExist` — in Compose
  Multiplatform Desktop the function is a member of SemanticsNodeInteraction,
  not a top-level extension function; callers use it as a method call directly
- Add `override` modifier to all four Settings method implementations in
  Android PlatformSettings (required when a class explicitly implements
  an interface via `: Settings`)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: guarantee isLoading=false when loadGraph job is cancelled pre-start

When scope.cancel() is called before the loadGraph coroutine reaches its
first suspension point, the CancellationException catch block may not run,
leaving isLoading=true indefinitely.

invokeOnCompletion fires unconditionally when the job ends — including for
cancellations that happen before the coroutine body executes — so isLoading
always resets to false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ios): replace JVM-only synchronized with KMP-compatible PlatformLock

LruCache.kt was updated on main to use synchronized(this) which is
JVM-only and breaks Kotlin/Native (iOS) compilation. Replace with an
expect/actual PlatformLock that uses ReentrantLock on JVM/Android and
a no-op on iOS/WASM where coroutine dispatcher scheduling provides
mutual exclusion within the DB context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(bench): benchmark summary b5a51ab

* chore(main): release 0.9.5 (#30)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(bench): benchmark summary 283753a

* chore: update Homebrew formula for v0.9.5

* chore(bench): benchmark summary 9bb8a77

* chore(main): release 0.10.0 (#34)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* chore(bench): android benchmark summary a1dfe06

* chore: update Homebrew formula for v0.10.0

* feat(android): extract Application class and share GraphManager across process lifecycle

Introduces SteleKitApplication to initialize the database driver and file system at
process start, so GraphManager survives Activity recreation without re-loading the graph.
Adds Glance AppWidget dependencies for upcoming home screen widget support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci(benchmark): retry push with rebase on non-fast-forward failure

When commits land on main while the benchmark job is running, the summary
push fails with a non-fast-forward error. Retry up to 5 times, pulling
with rebase before each attempt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(bench): benchmark summary 962b647

* fix(search): create-page in link picker now inserts link and appears first

The onCreatePage callback in PageView and JournalsView was a no-op,
so selecting "Create page" from the link picker did nothing. Wire it to
insertLinkAtCursor — same behaviour as selecting an existing page.

Also prepend CreatePageItem instead of appending so it is the first
(default-selected) result rather than buried below all page/block hits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(fdroid): enforce latest version, add metadata, replace placeholder icons

Three issues prevented the F-Droid store from showing correct information:

1. versionCode was hardcoded to 1 — F-Droid had no way to rank APKs, so it
   suggested the oldest release (v0.1.0). Now computed as
   major×1_000_000 + minor×1_000 + patch, coerced ≥2 so all future APKs
   outrank the historical versionCode=1 builds.

2. No fdroid/metadata/ directory — the store showed no description or links.
   Added fdroid/metadata/dev.stapler.stelekit.yml with name, summary,
   full description, license, and source/issue-tracker URLs.

3. All mipmap ic_launcher_foreground.png, ic_launcher.png, ic_launcher_round.png,
   and ic_launcher_monochrome.png were identical 3 KB placeholder files regardless
   of density. Replaced with the SteleKit brand icon (from assets/brand/png/icon_512.png)
   resized to the correct density-proportional dimensions, with the adaptive
   foreground respecting the 72/108dp safe zone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant