recordIncoming_after_close_does_not_schedule_persist called advanceUntilIdle()
while RelayHealthStore's init ticker (`while(true){ reclassify(); delay(60s) }`)
was still live on the shared StandardTestDispatcher scheduler. advanceUntilIdle()
chases that periodic delay forever, so the test spun at 100% CPU and never
returned — wedging :commons:jvmTest at "373 tests completed" and hanging the
pre-push hook (and leaving orphaned, CPU-pegging Gradle test workers behind).
Advance just past PERSIST_DEBOUNCE_MS and runCurrent() instead, so init's
debounced save fires for the baseline while the 60s ticker stays parked. The
post-close advanceUntilIdle() calls are fine — close() cancels the ticker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before Tor finishes bootstrapping, the relay pool dialed every Tor-routed
relay against the not-yet-listening SOCKS proxy. On a cold start this was
~580 doomed dials (all "SOCKS: Connection refused") concentrated in the
seconds before Tor went Active, churning sockets/CPU and inflating each
relay's backoff. The cost scaled with bootstrap latency, and the same
storm recurred on every network switch (which resets and re-bootstraps Arti).
Add an optional WebsocketBuilder.canConnect(url) gate (defaults to true,
so other implementors are untouched), checked at the top of
BasicRelayClient.connect() before the mutex/onConnecting/build — so a
gated relay opens no socket, fires no listener events, and grows no
backoff. The Android builder gates Tor-routed relays on
torManager.isSocksReady(); RelayProxyClientConnector already reconnects
them with ignoreRetryDelays=true the instant Tor flips to Active, so they
dial as soon as the transport is usable.
Measured on-device: pre-ready doomed Tor dials 581 -> 0 across cold starts
and WiFi<->Mobile switches; clearnet connections stay untouched and Tor
relays self-heal once Tor is Active.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Vitor's review of #3221: wake-detection is platform-specific UX, not
NostrClient's job. Quartz already exposes `reconnect(onlyIfChanged = false,
ignoreRetryDelays = true)` which does the full disconnect + connect — the
app layer just needs to call it when it detects a wake.
- Revert the keep-alive heuristic in NostrClient.kt; the loop is back to the
conservative `reconnectIfNeedsTo` path it had before.
- Add `runSleepResumeMonitor` (desktopApp/network/SleepResumeMonitor.kt): a
60s tick that watches for wall-clock overshoot and calls the supplied
`onWake` lambda. No native deps.
- Wire it in `Main.kt` next to the metrics LaunchedEffect: on >5x overshoot
call `relayManager.client.reconnect(onlyIfChanged = false,
ignoreRetryDelays = true)`.
Real OS sleep events (NSWorkspace on macOS, D-Bus PrepareForSleep on Linux,
WM_POWERBROADCAST on Windows) can be layered in later as platform improvements
without touching Quartz again.
Follow-up to #3186, addressing the unresolved review feedback:
- RelayHealthStore.schedulePersist() wrapped the blocking save() in withContext(ioDispatcher)
so prefs.flush() no longer sits on the Compose composition thread on Desktop.
- close() now fires the final save on a detached IO-bound scope instead of blocking
the composition thread for ~50ms during account switch / app exit.
- @Volatile on persistJob/tickJob and a closed-flag guard so the relay-network thread
and composition thread no longer race on plain vars (and post-close work is dropped).
- desktopApp/Main.kt passes Dispatchers.IO to RelayHealthStore so persistence flushes
land on the IO dispatcher instead of Dispatchers.Default.
Plus a separate-but-related fix to the offline-banner-stuck-after-Mac-sleep issue:
NostrClient.keepAliveJob now tracks wall-clock overshoot of its scheduled tick.
If the OS suspended us (laptop lid closed, system sleep), delay() returns far
past its deadline and the OkHttp websockets we held are dead even though
BasicRelayClient.isConnected() still reads true until the next ping fails.
On a >5x interval overshoot, force relayPool.disconnect() + connect() instead
of trusting needsToReconnect(), so feeds resume without an app restart.
A relay that accepts the WebSocket handshake and then immediately resets
the connection (e.g. essayist.decentnewsroom.com) defeated the exponential
reconnect backoff.
UrlParser.parseValidUrls filtered every detected URL through
isValidTopLevelDomain(), which requires the TLD's first character to be
an ASCII letter. IPv6 literal hosts are bracketed (e.g. [2001:db8::1])
and have no dotted TLD, so the whole bracketed host became the candidate
"TLD", starting with '[' and failing the check. As a result, valid
IPv6 URLs like http://[302:68d0:f0d5:b88d::bdb]/<hash> were dropped and
rendered as plain text instead of links.
The UrlDetector already validates the bracketed address as syntactically
correct IPv6, so accept bracketed hosts directly in isValidTopLevelDomain.
The "Updates all dependencies" commit (ece719445) bumped Kotlin to 2.4.0,
which removed the `DisableCacheInKotlinVersion.2_3_21` enum value. Rather
than bumping the guard, the whole `disableUiKitPrebuiltCache()` workaround
was deleted — which reintroduced the iOS test link failure on CI
Drop three lower-impact items from the Highlights list (still documented
in the body): audio visualizer (author notes minimal visibility), LaTeX
math (niche), and pinned DMs (minor QoL). Keeps the headline payment,
privacy, and new-feed features front and center.
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
Reviewed all merged PRs since the v1.11.0 release. Fixed the misleading
'new money operations' Tor line (the toggle already existed; what changed
is relay sockets now honor it). Added gaps the PR titles surfaced: NIP-14
group DM subjects (#3163), removal of wallet reordering drag-and-drop
(#3114), and diagnosable relay failure logs (#3197).
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
A file-level audit (not just commit subjects) surfaced major features hidden
under misleading commit messages: the new NIP-60 Cashu ecash wallet, music
tracks/playlists, the NIP-82 Software Apps directory, the live audio
visualizer, and multi-wallet management (NWC/CLINK-debit/Cashu).
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
Adds Hidden Words unblock, Share to DM, reply-to-zaps from notifications,
the Selected/Global notification split, the podcasts subsystem, desktop
unhealthy-relay review, the live zap-popup rails, and the commons KMP
migration scope.
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
Summarizes the 506 commits / 131 PRs merged since the v1.11.0 release:
CLINK payments, private posts/reactions via NIP-17, NIP-101e workouts,
LaTeX math rendering, NIP-89 app recommendations, pinned DMs, activity
cards, desktop image compression + new media player, and the Quartz
relay-server toolkit. User-facing sections are written for end users;
build/quartz/cli sections keep developer-level language.
https://claude.ai/code/session_01Y8cDkDRV48GYdQd3CR4kuV
OkHttp 5.4.0 expanded Interceptor.Chain from ~10 to ~40 members, breaking
the test's hand-rolled `object : Interceptor.Chain` fake. Replace it with a
java.lang.reflect.Proxy that implements only request()/proceed() and throws
for the rest, so it survives future interface growth without dozens of empty
stubs. A real OkHttpClient can't be used here because Platform init fails
outside Robolectric (android.util.Log.isLoggable is unavailable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Namecoin diagnostics card that Android renders below the
per-server test results to the desktop settings panel, so support
requests carry the same information on both platforms.
- last test timestamp + pass/fail tally
- host OS (name/version/arch) — desktop equivalent of Android's
Build.MANUFACTURER/MODEL row
- JVM name + version — desktop equivalent of Android's API level row
- distinct TLS versions observed during the test run
Also adds a 'Testing next server...' inline progress row matching the
Android section's behaviour when the test loop is mid-run.
Pure UI addition; no model, preference, or callback changes.
The single CHANGELOG.md had grown to ~7.9k lines. Move it to
docs/changelog/ with one file per release, named with zero-padded
minor/patch versions (e.g. v1.11.00.md) so plain file browsers sort
them correctly. Add a README.md index (newest first) and a TEMPLATE.md
for cutting new releases. The root CHANGELOG.md now points to the new
location.
The RelayStat.lastConnectAt / lastIncomingAt fields added in #3186 were
written on every connect and every incoming relay message (a TimeUtils.now()
call plus a volatile store on the Android hot path) but never read.