Commit Graph
14137 Commits
Author SHA1 Message Date
Claude db2b9551f9 fix(user-metadata): fall back to indexer relays when outbox is exhausted
If a user's NIP-65 outbox advertises only relays that don't hold their
kind 0, profile fetching used to give up after EOSE on those relays.
filterUserMetadataForKey now widens to the account's indexer relays
once every outbox relay has either EOSE'd or is in cannotConnectRelays
and metadata is still missing. UserWatcherSubAssembler invalidates
filters on EOSE so the fallback re-evaluates without waiting for an
unrelated trigger.
2026-05-27 01:20:37 +00:00
Vitor PamplonaandGitHub f6066de92a Merge pull request #3054 from vitorpamplona/claude/pensive-brown-UVDZf
Replace wallet reorder buttons with drag-and-drop UI
2026-05-26 20:33:59 -04:00
Claude 906ac06c57 feat(wallet): drag-and-drop reorder for NWC wallet cards
Replace the up/down chevron IconButtons on each wallet card with a drag
handle, matching the pattern used across the relay-settings screens.
Reuses RelayDragState / rememberRelayDragState / draggableRelayItem /
relayDragHandle from relays/common — same gesture handling, elevation
animation, and swap-on-threshold behavior.

The handle and item modifier are only attached when there is more than
one wallet to reorder.
2026-05-27 00:10:05 +00:00
Claude 290a6b1f85 fix(nwc): scope Send/Receive/Transactions to the wallet shown in the detail screen
The wallet detail screen's Send, Receive and Transactions buttons navigated to
parameterless routes. Each destination created a fresh WalletViewModel with no
selection, so the action ran against `_defaultWalletId` (the account default)
instead of the wallet being viewed. Paying, invoicing, and listing
transactions could therefore go to the wrong wallet.

Parameterize WalletSend/WalletReceive/WalletTransactions with `walletId`,
plumb it through AppNavigation, pass it from WalletDetailScreen, and have
each screen call `selectWallet(walletId)` before operating.
2026-05-27 00:05:55 +00:00
Vitor Pamplona 5fce6764b5 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-26 18:48:06 -04:00
Vitor PamplonaandClaude Opus 4.7 cdb5e01821 fix(nwc): re-add #p to response filter for Alby relay routing
Dropping both `authors` and `#p` from the kind-23195 subscription filter
fixed wallets that don't set those fields the way NIP-47 implies, but
broke purpose-built NWC relays (notably relay.getalby.com/v1) that use
`#p` as the routing key — without it the relay never delivers the
response to our subscription, so the wallet screen sits on a spinner.

Restore `#p: [client pubkey]` in the relay filter. Keep `authors` out
since that field was the one actually causing the broader interop pain.
Spec-compliant responses always carry the `p` tag, so adding it back
does not exclude any conforming wallet. End-to-end authenticity is
still enforced by NIP-04 decryption against the per-connection shared
secret and by the client-side author check in NwcPaymentTracker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:44:14 -04:00
Vitor Pamplona 2dd0166fee Better checks the id and sig before verifying the event. 2026-05-26 18:42:12 -04:00
Vitor PamplonaandGitHub f3ac87689a Merge pull request #3053 from vitorpamplona/claude/tor-stops-working-1PIcU
Add Tor self-heal watchdog + integration tests + Arti v2.3.0
2026-05-26 17:52:54 -04:00
Claude 2c89a62789 test(tor): expand tier-3 to verify each root cause of the wall-and-stop bug
Bug had four ingredients (per the kdoc on TorArtiNativeIntegrationTest).
We had one test for #1; now there's targeted coverage for each:

1) Native TorClient gets stuck (bad guards / dead circuits / expired
   consensus) with no way to drop it in-process:
   `destroy then re-initialize releases the state file lock cleanly`
   (was already there — added exit-IP logging so the developer can eyeball
   that the circuit actually changed across the destroy).

2) In-flight per-connection handlers holding Arc<TorClient> clones,
   pinning the state file lock past destroy:
   NEW `destroy aborts an in-flight SOCKS handler quickly`
   Opens a SOCKS HTTPS request, lets the handler get into the data plane,
   calls destroy() concurrently, asserts:
     - destroy() returns within 3s (the budgeted abort+sleep window),
     - the in-flight request thread dies within 5s,
     - a fresh initialize on the SAME data dir succeeds afterward
       (this is the actual regression net — pre-fix the orphaned handler's
       Arc would keep the TorClient alive and the lock held).

3) stopSocksProxy *deliberately* preserves the running client so the
   legitimate stop/start toggle is fast. We need to keep that path
   working after the destroy/abort changes:
   NEW `stopSocksProxy then startSocksProxy reuses the running TorClient`
   Asserts the second startSocksProxy returns in < 5s — no re-bootstrap.

4) State / fd / memory leaks accumulating across many destroy/init cycles
   (the watchdog can drive these forever):
   NEW `survives multiple destroy then initialize cycles`
   5 full cycles of initialize → startSocksProxy → fetch → destroy.
   Logs per-cycle elapsed time + exit IP so degradation is observable
   even when it's not yet a hard failure.

Plus two extra robustness tests:

NEW `proxies concurrent SOCKS requests in parallel`
   5 in-flight HTTPS-via-SOCKS requests at once. Exercises the Rust
   accept loop, HANDLER_TASKS retain-on-push, and Arc<TorClient> clone
   independence under load.

NEW `destroy is idempotent`
   destroy-without-init, double-destroy, init-after-double-destroy.
   Cheap regression net for unwrap-on-None panics in the Rust shim.

All new tests gated by -Pamethyst.arti.integration=true alongside the
existing ones; the smoke test (`library loads and reports a version`)
still runs unconditionally on Linux x86_64. Total runtime for the slow
suite is ~10-15 minutes against Tor, depending on bootstrap luck.
2026-05-26 21:43:14 +00:00
Claude e39ea55fd6 test(tor): tier-3 integration — JVM host build of Arti, smoke + bootstrap tests
Closes the test gap below the tier-1 unit tests by running the real Arti
JNI shim end-to-end on JVM. Cheaper than an emulator + connectedAndroidTest,
and exercises the exact Rust + JNI code path the Android .so does.

Three tests in TorArtiNativeIntegrationTest:

1. `library loads and reports a version` — always-on smoke check. Loads
   libarti_android.so via System.loadLibrary and calls ArtiNative.getVersion.
   ~10ms. Catches build/link regressions (e.g. a stale .so after an ARTI
   bump, a missing JNI symbol export, a forgotten rebuild on this path).
   Skipped on non-Linux-x86_64 hosts with a clear message pointing at the
   build-arti-host.sh rebuild step.

2. `bootstraps and proxies an HTTPS request through Tor` — opt-in via
   -Pamethyst.arti.integration=true. ArtiNative.initialize → startSocksProxy
   → OkHttp-via-SOCKS → check.torproject.org/api/ip. Asserts "IsTor":true.
   Regression net for the rustls CryptoProvider install we added after the
   v2.3.0 bump and for the destroy/handler-abort fixes in the Rust shim.

3. `destroy then re-initialize releases the state file lock cleanly` — opt-in.
   The direct unit-test mirror of the self-heal path: bootstrap, destroy, hit
   the SAME data dir with initialize again, verify it succeeds without a
   "state file already locked" error and that traffic still flows.

Wiring:
- New tools/arti-build/build-arti-host.sh — companion to build-arti.sh.
  Cargo-builds the wrapper crate for the host target (x86_64-linux on most
  dev machines, but the script maps macOS / arm64-linux too) and copies to
  amethyst/src/test/native-libs/<host-tag>/libarti_android.so.
- amethyst/build.gradle.kts testOptions.unitTests.all configures
  -Djava.library.path so System.loadLibrary("arti_android") finds the
  checked-in host .so. Also forwards -Pamethyst.arti.integration so the
  opt-in gate works from a Gradle invocation.
- Checked-in src/test/native-libs/x86_64-linux/libarti_android.so for the
  most common dev/CI host (~6 MB).

Wrapper change to make the JVM path actually run:
- lib.rs: on #[cfg(not(target_os = "android"))], call
  builder.storage().permissions().dangerously_trust_everyone() so Arti's
  fs-mistrust check doesn't reject /tmp data dirs on hosts where parent
  directories have unusual UIDs (typical in containers). Android keeps its
  strict default — the app's private filesDir is already sandboxed by the OS.
  Compiled-out on Android, so the shipped Android .so is functionally
  unchanged.

Verified in this session:
- Smoke test passes without -P (3 tests, 1 ran, 2 skipped).
- Full unit test suite still passes.
- With -P the bootstrap tests get past Arti's permissions check; they hang
  on actual relay I/O in this container because outbound TCP egress is
  restricted to a CDN allow-list, not Tor relays. Tests succeed on hosts
  with unrestricted outbound — see the test kdoc.
2026-05-26 21:34:26 +00:00
Vitor PamplonaandGitHub f2bfd7a315 Merge pull request #3052 from vitorpamplona/claude/brave-clarke-hJ0PK
onchain zaps + nip-05 filter when returning users to Gemini
2026-05-26 17:22:25 -04:00
Claude 93163141b9 feat(amethyst): anti-impersonation safeguards on AppFunctions write verbs
Zaps and DMs move real artifacts (money, private messages) to a Nostr
pubkey. Nostr has no global namespace, so "zap Alice" is ambiguous —
multiple users can publish the same display name. Four safeguards now
make it much harder for Gemini (or any agent) to misroute a write:

1. `expectedDisplayName: String?` on followUser / sendDm / zapUser.
   Agent passes the name it understood; verb cross-checks that the
   resolved profile's name / display name / NIP-05 contains it (or
   vice-versa). Mismatch aborts with a typed error carrying the npub
   and NIP-05 so the agent can re-prompt.

2. `requireFollow: Boolean = true` default on sendDm and zapUser.
   Refuses to act on a pubkey the user doesn't already follow on
   Nostr. Strongest guard against same-name impersonators — even if
   the agent picked the wrong Alice, the user almost certainly isn't
   following her. Override to false only when the user explicitly
   approves acting on a stranger.

3. Updated kdocs instruct the agent to confirm with the user using
   all three identity signals (display name + npub + NIP-05) before
   invoking. The kdoc is what Gemini reads to learn the verb's
   contract, so this is where the instruction goes.

4. searchProfiles now filters out hits whose NIP-05 claim explicitly
   fails verification (the listed domain refuses to sign for that
   pubkey). Network errors / no-claim profiles are kept (inconclusive,
   not refutations). Verifications run in parallel with a 4s overall
   budget; on timeout we surface all candidates rather than censor.

https://claude.ai/code/session_013NKVhEF2KqyCrV7ufaiQ6N
2026-05-26 21:07:38 +00:00
Claude 3517606b81 test(tor): tier-1 TorManager unit tests + tier-3 instrumented scaffold
Tier 1 — 18 fast unit tests for the self-heal logic, virtual time only:
- Extracted TorBackend interface (status + start/stop/reset/resetWithCleanState),
  TorService implements it. TorManager now takes a TorBackend by injection
  rather than constructing a TorService itself.
- Extracted TorPreferencesPort (torType + externalSocksPort flows + load/save
  bypass-approval). TorSharedPreferences implements it via forwarding properties.
- Injected ioDispatcher (default Dispatchers.IO) and nowMs clock (default
  System::currentTimeMillis) so tests drive the 45s watchdog + 5-min cooldown
  in milliseconds of virtual time.
- Tests cover: persisted-approval load, torType-change bypass clear,
  approveBypassForOneHour, onNetworkChange (clear + reset + cooldown prime),
  watchdog gentle-reset before first Active, watchdog full-reset after Active,
  watchdog cancellation on Active, cooldown blocks within window + permits
  outside, status routing for OFF/EXTERNAL/INTERNAL, sessionBypass forcing Off,
  activePortOrNull mirroring.
- Uses UnconfinedTestDispatcher inside runTest — flowOn(ioDispatcher) +
  WhileSubscribed cross-dispatcher channel needs eager dispatch for
  MutableStateFlow.value updates to propagate through advanceUntilIdle.

Tier 3 — TorBootstrapInstrumentedTest scaffold (@LargeTest, @Ignore by default):
- Cold-start bootstrap: TorService.start → first { Active } within 120s.
- HTTPS round-trip: OkHttp via SOCKS to check.torproject.org, asserts IsTor:true.
  This is the regression net for the rustls CryptoProvider install after the
  Arti bump and for the destroy/handler abort race in the Rust shim.
- reset → re-start: verifies the state-file-lock is released so the second
  TorService.start can re-create the TorClient cleanly.
- KDoc documents how to enable + run on a real device (the test needs Tor
  network egress + 60–120s of wall-clock per case, hence default-Ignored).

No production behavior changes — only injection seams + interfaces.
2026-05-26 20:31:32 +00:00
Claude 612e05fa62 feat(amethyst): zapUser supports onchain (NIP-BC) rail
Adds a `chain` parameter to zapUser so Gemini can route the zap over
Lightning (default) or onchain Bitcoin (NIP-BC kind:8333).

  * chain="lightning" (or null, "ln") — existing Lightning flow:
    build kind:9734 → fetch BOLT11 → NWC auto-pay if configured →
    return invoice + nwc fields.
  * chain="onchain" (or "btc", "bitcoin") — new path:
    1. Require the user's Bitcoin chain backend to be configured
       in Amethyst Settings → Bitcoin. Throw NotSupported with a
       pointer to the settings screen if absent.
    2. Validate feeRateSatPerVByte (0.1 ≤ rate ≤ 1000).
    3. Call Account.sendOnchainZap, which uses the existing
       OnchainZapSender pipeline: build P2TR-paying tx, sign,
       broadcast, publish kind:8333 receipt.
    4. Return ZapResult with onchainTxid + feeSats + changeSats +
       receiptEventId on success, or onchainError + onchainStage on
       failure. When the failure stage is "publishing" the tx is
       already on-chain — we still surface broadcastTxid so the user
       can verify on a block explorer.

ZapResult gains a `chain` discriminator field plus six onchain*
fields. Lightning zaps populate the existing fields and null out
the onchain ones; onchain zaps do the inverse. The kdoc on ZapResult
explains the split.

Trigger phrases in the verb kdoc now include "send N sats onchain to
[user]" / "send Alice N sats via Bitcoin" so Gemini's matcher picks
up the onchain intent specifically.

New parameters:
  * chain: String? = null — "lightning" | "onchain" (case-insensitive)
  * feeRateSatPerVByte: Double = 5.0 — fee rate for onchain rail,
    ignored for Lightning. 5 sat/vB targets fast confirmation under
    typical mempool conditions without being aggressive.

zapEvent stays Lightning-only for now — onchain event zaps with
NIP-57 splits need a different pipeline (sendOnchainZapWithSplits)
and the result shape would be quite different. Deferred to a
follow-up if there's demand.

app_metadata.xml updated so the LLM picker pitches the dual-rail
capability to users.
2026-05-26 20:31:29 +00:00
Claude 9a2adf091d chore(tor): bump Arti to v2.3.0
Wins: reduced GeoIP memory usage (moved off heap), CircuitClosed→NotConnected
error change (affects our handler error paths), DATA-cells-on-closed-streams
fix, and a flow-control sidechannel mitigation bug fix. Nothing here directly
addresses the stuck-Tor recovery work in the prior commits, but it's a clean
overdue bump while we're in this code.

Wrapper changes required by the bump:
- arti-client + tor-rtcompat: 0.41 → 0.42 to match the new crate versions
  shipped with arti-v2.3.0.
- arti-v2.3.0's tor-rtcompat no longer installs a rustls CryptoProvider
  implicitly (changelog: "if the application fails to install a rustls
  CryptoProvider, tor-rtcompat no longer installs one itself"). Add a direct
  `rustls = "0.23"` dep with the `ring` feature and `install_default()` it
  inside INIT_ONCE before runtime creation — otherwise create_bootstrapped
  panics on the first TLS handshake. Keeping `ring` (same as 2.2.0
  effectively used) rather than 2.3.0's new default `aws-lc-rs`, which is
  heavier on Android and has known build.rs pain on aarch64-linux-android.

Heads-up for the next bump: arti-v2.4.0 will explicitly wrap TorClient in
Arc rather than implicitly having Arc-like semantics. We already wrap
explicitly so the migration is a no-op aside from potential Arc<Arc<...>>
cleanup.

Rebuilds: libarti_android.so for arm64-v8a + x86_64.
2026-05-26 20:06:15 +00:00
Claude c3ddd4e7be fix(tor): audit fixes — first-bootstrap grace + tighten destroy() race
Audit of db378a1 surfaced three issues; this commit addresses them.

1) First-bootstrap self-heal storm (TorManager). On a fresh install with a
   slow network the legitimate first bootstrap takes 30–60s. The 45s
   stuck-Connecting watchdog used to fire resetWithCleanState, wiping an
   empty state dir and adding a full bootstrap cycle of delay for no gain.
   Now: track hasEverBootstrapped (flipped when status reaches Active);
   pre-first-bootstrap self-heals use the gentler reset (drop client only,
   keep state), post-first-bootstrap use resetWithCleanState. Wiping stale
   on-disk guards only matters once we know Arti can actually work.

2) Rust destroy() race (lib.rs). The accept loop in startSocksProxy has no
   .await between accept() returning and HANDLER_TASKS.push(h), so an
   abort() alone is racy — a new handler can be spawned and pushed AFTER
   our drain runs, which then holds an Arc<TorClient> past destroy() and
   keeps the state file lock alive. Now: after abort(), await the SOCKS
   JoinHandle with a 1s timeout so the listener fully terminates before
   we drain HANDLER_TASKS. No new handlers can be added once the listener
   is gone.

3) TOKIO_RUNTIME mutex held during block_on(sleep). The previous
   `if let Some(rt) = TOKIO_RUNTIME.lock().unwrap().as_ref()` kept the
   mutex held for the full sleep duration, blocking any other JNI caller
   that needs the runtime. Now: clone the runtime Handle and release the
   mutex immediately. Same fix applied to stopSocksProxy.

Rebuilds: libarti_android.so for arm64-v8a + x86_64.
2026-05-26 19:37:36 +00:00
Claude db378a105c feat(tor): self-heal — drop & rebuild Arti on network change and stuck Connecting
When Arti's in-memory TorClient gets into a broken state (bad guards from a
previous network, dead circuits, expired consensus held in memory), nothing
short of a process restart used to recover it: the JNI exposed initialize /
startSocksProxy / stopSocksProxy but no way to drop the TorClient, and the
Kotlin side gated initialize behind a one-shot AtomicBoolean. force-stop
preserved the on-disk arti/state/, toggle-off-then-on only re-bound the SOCKS
listener on the same broken client, and wiping app data was the only way out.

Rust side
- New JNI Java_..._ArtiNative_destroy: aborts the SOCKS listener task, aborts
  all in-flight per-connection handlers (each holds an Arc<TorClient> clone
  that would otherwise pin the state file lock), waits 500ms, drops the static
  ARTI_CLIENT. Next initialize() call creates a fresh client and re-bootstraps.
- Track handler JoinHandles in HANDLER_TASKS so destroy can abort them; cull
  finished ones on each accept to keep the Vec bounded.

Kotlin side
- TorService.reset() / resetWithCleanState() — drop the native client, flip
  initialized=false. The second variant also wipes arti/state/ on disk to
  rebuild guard selection from scratch.
- TorManager.resetEpoch StateFlow is now part of the status combine; bumping
  it re-fires the INTERNAL branch which calls service.start() and runs full
  Arti re-init.
- onNetworkChange (wired from ConnectivityManager.networkId distinctUntilChanged)
  now calls service.reset() + clears the persisted bypass approval + bumps the
  epoch. Replaces the previous clearSessionBypass() which only touched the
  in-memory bypass half.
- Self-heal watchdog: when status sits at Connecting for >45s (before the 60s
  connectionFailure dialog), calls resetWithCleanState. Rate-limited to one
  per 5 minutes so a permanently broken network doesn't loop us. onNetworkChange
  primes lastSelfHealAtMs so a slow legitimate post-network-change bootstrap
  doesn't get a second reset on top of itself.

Rebuilds: libarti_android.so for arm64-v8a + x86_64 (NDK 27, 16KB-page aligned).
2026-05-26 19:09:54 +00:00
Claude 321adebfe6 fix(tor): clear remembered-approval window on user TorType toggle
Once the user picked "Use regular connection" after a 60s stuck-Connecting
prompt, `lastBypassApprovalMs` was persisted to DataStore for an hour. Inside
that window the connection-failure flow silently flipped `sessionBypass = true`
on every later Connecting span instead of re-prompting, which caused the
status flow to call `service.stop()` and emit `Off` regardless of the user's
`TorType`. The DataStore-backed approval survived force-stop, and toggling
Tor off/on only cleared the in-memory `sessionBypass` half — so the next
bootstrap attempt re-triggered the silent bypass after 60s and the user was
trapped until wiping app data.

Any user-initiated `TorType` change now wipes both halves: the in-memory
`sessionBypass` flag and the persisted approval. The next stuck-Connecting
span will surface the dialog again so the user has a real choice instead of
a silent fall-back to direct.
2026-05-26 17:54:04 +00:00
Vitor PamplonaandGitHub b258028b54 Merge pull request #3051 from vitorpamplona/claude/brave-clarke-hJ0PK
Phase 2: Gemini AppFunctions bridge + CLI action verbs
2026-05-26 12:23:57 -04:00
Claude 6671cb3cbc refactor(amethyst): getFeedDigest now uses HomeNewThreadFeedFilter — mirrors the home page exactly
User feedback: returning "summarize my feed" results that didn't match
what's actually on the home page is misleading. Now the verb invokes
the same `HomeNewThreadFeedFilter` the foreground UI uses, against the
same LocalCache, so the LLM sees what the user would see if they
opened Amethyst.

What this fixes:
  * Reposts, polls, long-form, comments, audio, etc. — the home filter
    accepts ~17 event kinds; the previous verb saw only kind:1.
  * Muted users — now filtered out.
  * Replies — excluded (top-level threads only, matching the UI).
  * Repost dedup — same note via multiple reposts collapses to one
    entry, as on the screen.
  * The user's currently-selected NIP-51 follow list (custom lists,
    hashtag feeds, communities) — now respected. Was previously
    hardcoded to plain kind:3.

Trade-off: reads from LocalCache, so the verb reflects what the
foreground has already pulled. If the user hasn't opened Amethyst in
a while, the digest is sparse. Acceptable for "summarize what I'm
seeing" semantics — for fresh data, the other verbs (search*,
getRecentFromFollows) do their own relay drain.

Implementation:
  * Event.toFeedNoteHit() generic projection — handles the broader
    event range with snippet-truncation for long content.
  * NoteHit gains a `kind: Int` field so the LLM can distinguish
    "Alice posted a note" from "Alice published an article" or
    "Alice ran a poll".
  * TextNoteEvent.toNoteHit() delegates to the generic helper.
  * searchArticles' inline NoteHit construction also folds into the
    generic helper — one less code path to maintain.

Plus amethyst/plans/2026-05-26-appfunctions-screens-as-verbs.md
documenting the broader pattern: every Amethyst screen has a
FeedContentState driven by a *FeedFilter; we'd add one AppFunction
verb per screen, all going through the same filter pipeline the UI
uses. Lists the ~25 unmapped feeds with proposed verb names so the
work has a clear roadmap. Same pipeline will back the future MCP
server.
2026-05-26 16:04:47 +00:00
Claude dc2c7f9be1 feat(amethyst): getFeedDigest verb — feed-summary surface for the LLM
New verb: getFeedDigest(hoursBack, maxNotes).

Use when the user asks "summarize my Nostr feed", "give me a digest
of what my follows posted today", "recap Nostr", or any other
summary / digest / recap intent.

Returns a structured snapshot for AI summary instead of a raw note
list: total note count, unique author count, top hashtags (≤10) and
top mentioned users (≤10) — with display names resolved from the
local kind:0 cache — alongside the trimmed note body. The LLM uses
the aggregate signals to write a one-paragraph "the conversation
focused on X, with N people posting about Y" instead of having to
re-derive frequencies from a raw list.

Implementation:
  * Shared core extracted into fetchFollowFeed(account, since, limit)
    so getRecentFromFollows and getFeedDigest don't duplicate the
    drain logic.
  * Over-fetches by 3× the visible cap so stats are computed over a
    larger sample than the LLM sees, capped at 500 events for bounded
    on-device work.
  * Hashtag bucketing: lowercases + strips leading #, so #Bitcoin
    and #bitcoin collapse.
  * Mention bucketing: skips self-mentions (some clients tag the
    author themself, not useful for the digest).

New @AppFunctionSerializable result types:
  * HashtagFrequency, MentionFrequency — count + identifier.
  * FeedDigestResult — windowHours, totalNoteCount, uniqueAuthorCount,
    topHashtags, topMentions, notes.

Total verb count: 22. app_metadata.xml updated so Gemini's tool
picker can pitch the summary surface specifically.

Known scope: currently returns kind:1 from the user's kind:3 follow
list — does NOT match the in-app home feed exactly. The home feed
includes reposts, long-form, polls, comments, etc., respects the
user's currently selected NIP-51 list, and filters muted users.
Aligning the digest to the home feed (via HomeNewThreadFeedFilter
against LocalCache) is a documented follow-up.
2026-05-26 15:58:30 +00:00
Claude 4617be2068 chore(amethyst): collapse unreachable NWC when branch
withTimeoutOrNull(deferred.await()) returns a flattened Response? —
both "timeout" and "wallet sent null" produce null, and we already
catch null via the elvis-return above. The explicit `null ->` arm in
the response switch was dead code; the compiler warned about it.
Folded the "wallet sent null we couldn't decrypt" case into the
timeout error message since they're indistinguishable to the caller.
2026-05-26 15:40:34 +00:00
Claude ee0942d555 Merge remote-tracking branch 'origin/main' into claude/brave-clarke-hJ0PK 2026-05-26 15:32:47 +00:00
David KasparandGitHub d1610bf976 Merge pull request #3048 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-26 16:24:44 +02:00
Crowdin Bot 153b9495da New Crowdin translations by GitHub Action 2026-05-26 14:21:22 +00:00
Vitor PamplonaandGitHub 9cd438570f Merge pull request #3050 from davotoula/fix/ios-build-failure
Complete Phase 2 KMP migration to unblock iOS CI
2026-05-26 10:19:16 -04:00
davotoula 28865f38c3 tests:
- cover CodePoints helpers and Channel.relays() equal-count behaviour
- Two new test files in commons/src/commonTest/, both run under :commons:jvmTest.
2026-05-26 15:53:37 +02:00
Claude d0f6739a30 feat(amethyst): NWC auto-pay for zapUser and zapEvent
Returning a BOLT11 invoice for the user to paste somewhere defeated
the point of "Gemini, zap Alice 21 sats". Now when the active account
has a Nostr Wallet Connect (NIP-47) wallet configured in Amethyst,
both zap verbs pay the invoice automatically over NIP-47 and report
the outcome inline.

Implementation:

  * payViaNwcOrNull(account, bolt11, zappedNote) — null when no NWC
    set up (caller falls back to manual). Otherwise wraps the
    callback-based Account.sendZapPaymentRequestFor in a
    CompletableDeferred + withTimeoutOrNull. 30s budget; if the
    wallet doesn't answer in that window the caller sees an
    nwcError of "wallet didn't respond within 30s" and still has
    the raw invoice to fall back on.

  * Decodes the wallet's response: PayInvoiceSuccessResponse carries
    the preimage, PayInvoiceErrorResponse carries a typed code +
    message, NwcErrorResponse covers transport-level errors, null
    means "couldn't decrypt the reply" (rare — wallet misconfigured
    or our signer rejected). Each case maps to a typed
    NwcOutcome the verbs can render.

  * ZapResult / ZapInvoice grow four fields: nwcAttempted, nwcPaid,
    nwcPreimage, nwcError. The invoice is still always returned so
    Gemini can show it as a manual-payment fallback when NWC isn't
    configured or rejects. zapEvent attempts each split independently
    — one wallet failure doesn't block the rest.

Kdoc updates note the NWC behavior so the LLM picks up "if NWC is
configured, this just works" — that's the user-visible promise of
asking Gemini to tip someone.
2026-05-26 13:37:05 +00:00
davotoula 771ba67f31 Code review:
- guard shared NSDateFormatter in formattedDateTime iOS actual
2026-05-26 14:53:38 +02:00
Vitor PamplonaandGitHub bb921ad643 Merge pull request #3049 from nrobi144/feat/desktop-rich-text-and-profile
feat(desktop): rich text migration, profile metadata, copy JSON, @mention autocomplete
2026-05-26 07:55:55 -04:00
davotoulaandClaude Opus 4.7 5ec60e285b fix(commons): unblock :commons iOS compile after Phase 2 target flip
PR #3047 enabled iosArm64 + iosSimulatorArm64 on :commons and added
:commons:compileKotlinIosSimulatorArm64 as a CI gate, but the Phase 2
migration was incomplete — JVM-only APIs survived in commonMain and
several expect declarations had no iOS actual. Every main CI run since
the merge failed at "Compile Commons for iOS".

Migrations in commonMain
- Dispatchers.IO: add `import kotlinx.coroutines.IO` to 16 files, matching
  the quartz/NostrClient.kt pattern (kotlinx-coroutines 1.11 exposes IO on
  Native via this import; no shim needed).
- synchronized {}: replace with the existing KmpLock + withLock in
  EOSECache, AcceptedGamesRegistry, EventDeduplicator, ThumbHashDecoder,
  PeerSessionManager. Restructure two PeerSessionManager methods that
  late-init vals from inside the lock — withLock returns a tuple now.
- Unicode code points: drop java.lang.Character / String.codePointAt /
  String.offsetByCodePoints. Add commons/util/CodePoints.kt with surrogate
  -pair-aware KMP helpers; rewrite EmojiCoder + EmojiUtils against them.
- Byte<->String: encodeToByteArray() / decodeToString() / concatToString()
  in EmojiCoder, Base83, BlurHashEncoder, RobohashAssembler,
  LongFormPublishAction (drops Charsets / String(CharArray) / toByteArray
  no-arg).
- Math.round → Double.roundToLong in BlurHashEncoder.
- String.format → Compose Resources stringResource(res, vararg) overload
  in LoadingState (FeedErrorState).
- toSortedSet → sortedByDescending { }.mapTo(LinkedHashSet) in Channel —
  preserves the descending-by-relay-count iteration order callers depend
  on.
- Comparator<T>: kotlin.Comparator on Native takes non-null T. Align
  CreatedAtComparator / CreatedAtComparatorAddresses to compare(a, b) and
  drop dead null checks in CreatedAtIdHexComparator.

iOS actuals (commons/src/iosMain/)
- WeakReference: switch from typealias to explicit `actual class`. The
  expect param is `referent` (matches java.lang.ref); kotlin.native.ref.
  WeakReference uses `referred`, so typealias fails the expect/actual
  name-match check on Native. Add @file:OptIn(ExperimentalNativeApi).
- PlatformImage: functional IntArray-backed actual (used by BlurHash and
  ThumbHash decoders at runtime); Phase 3 will swap to CGImage.
- ChessDismissedGamesStorage: in-memory only; NSUserDefaults wiring lands
  with iosApp in Phase 3.
- SecureKeyStorage: stub throwing SecureStorageException. Keychain
  Services binding is Phase 4 per the iOS plan.
- formattedDateTime: NSDateFormatter with "yyyy-MM-dd-HH:mm:ss" + POSIX
  locale + local time zone (semantically matches the JVM
  DateTimeFormatter "uuuu-MM-dd-HH:mm:ss" for post-1970 timestamps).
- checkNotInMainThread: no-op (mirrors jvmMain).
- PlatformNumberFormatter: NSNumberFormatter(.DecimalStyle), with
  NSNumber.numberWithLongLong to disambiguate the NSNumber(Long)
  overload set.
- isDebug: false constant; iosApp can flip via Swift `DEBUG` flag later.

Verified locally
- :commons:compileKotlinIosSimulatorArm64 + compileKotlinIosArm64 green
- :quartz:iosSimulatorArm64Test green
- :commons:jvmTest + :quartz:jvmTest green (no JVM regression)
- :quartz:verifyKmpPurity + :commons:verifyKmpPurity + spotlessCheck green

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:38:01 +02:00
davotoulaandClaude Opus 4.7 027808ae54 skills(find-missing-translations): filter out keys Crowdin already owns
A "missing" key in values-<locale>/strings.xml is not always actionable:
Crowdin omits source-identical translations on export (translator chose
"use English" for brand terms like "Nowhere X", loanwords like "Apps",
or version prefixes like "v%1$s"). Adding source-identical fallbacks
locally is noise that the next Crowdin sync strips again; Android already
falls back to values/strings.xml at runtime.

Add a Step 2.5 sync-timestamp filter that uses the latest
"New Crowdin translations by GitHub Action" commit reachable from HEAD as
the cutoff. Keys added to values/strings.xml after that commit are
genuinely new (Crowdin hasn't exported them yet); anything older is
Crowdin's responsibility. The reachable-from-HEAD check survives the
common workflow of deleting the l10n_crowdin_translations branch after
merging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 09:20:51 +02:00
Claude 5efb5d90e5 feat(amethyst): zap verbs + LLM-friendly kdocs + Gemini discovery plan
Three deliverables:

1) Two new write verbs:
   * zapUser(user, sats, comment?) — builds the NIP-57 kind:9734
     profile zap and fetches a BOLT11 invoice from the recipient's
     Lightning service. Returns the invoice — caller pastes into a
     Lightning wallet (no NWC auto-pay yet). 21 sats default,
     1M sats cap, 280-char comment cap.
   * zapEvent(eventId, sats, comment?) — same but for a specific
     note, with full NIP-57 zap-split support via
     ZapActions.buildEventZapRequestsForSplits. Returns one invoice
     per recipient when the post carries `zap` tags.

   Total verb count: 21 (8 read for feeds/profiles, 3 read for
   identity / followers, 4 read for inbox/zaps/streams, 4 write
   for note/follow/unfollow/dm, 2 write for zaps).

2) Reworked every verb's kdoc first sentence into an LLM-friendly
   "use when..." trigger phrase. Gemini's tool picker matches user
   queries against the descriptions (we generate them via
   @AppFunction(isDescribedByKDoc = true)) — phrasing like "Find a
   person on Nostr by name. Use when the user wants to look someone
   up..." gives the model concrete prompts to recognise instead of
   internal NIP names.

   Affected: searchProfiles, getRecentFromFollows, getNotesByUser,
   getProfile, searchByHashtag, getActiveAccountInfo, getRecentDms,
   getZapsReceived, postNote, followUser, unfollowUser, sendDm,
   zapUser, zapEvent.

3) amethyst/plans/2026-05-26-appfunctions-gemini-discovery.md —
   verification protocol for testing on-device whether Gemini's
   tool picker actually surfaces our verbs from natural-language
   prompts. Includes specific test prompts mapped to expected
   verbs, fallback diagnostics (clear AppSearch + restart), and
   the conditions under which it'd be worth defining our own
   @AppFunctionSchemaDefinition namespace.

Plus minor: comment parameters switched to nullable (String? = null)
because KSP rejects non-nullable types with defaults.
2026-05-26 00:01:13 +00:00
Claude 8e1b31a550 feat(amethyst): four write verbs for Gemini — postNote, follow, unfollow, sendDm
Phase 4 from the signer-prompt plan, scoped to Option B (refuse NIP-55
with a typed NotSupportedException). Internal-key and NIP-46 bunker
accounts can now publish from Gemini.

New @AppFunction methods:

  * postNote(text) — kind:1 short text note. Caps at 8000 chars to
    catch accidentally-pasted documents; publishes to outbox relays
    with per-relay ack reported.

  * followUser(user) / unfollowUser(user) — kind:3 contact list
    update via FollowActions. Detects already-following / not-
    following and returns WriteResult.unchanged() rather than
    re-publishing the same kind:3. New follows stamp the relay hint
    from the target's cached kind:10002 write list, mirroring
    User.bestRelayHint().

  * sendDm(recipient, text) — NIP-17 gift-wrap via DmActions.buildTextDm.
    Resolves per-recipient relay set through DmActions.resolveDmRelays
    (permissive mode — falls back through NIP-65 read to bootstrap so
    Gemini users don't trip on the strict kind:10050 rule). Returns
    one DmDelivery per wrap (recipient + sender's own copy).

Signer gating — requireInProcessSigner():
  * Read-only signers (npub-only login) → AppFunctionNotSupportedException
    "sign in with a private key or NIP-46 bunker to publish".
  * NIP-55 external signers (Amber) → AppFunctionNotSupportedException
    "open Amethyst directly to complete the action". Detected via
    qualified class name to avoid hard-coupling the bridge to the
    nip55AndroidSigner module.
  * NostrSignerInternal / NostrSignerRemote — sign in-process; the
    NIP-46 round-trip already suspends through .sign(), no special
    handling needed.

New @AppFunctionSerializable types:
  * WriteResult — { changed, eventId?, publishedTo, rejectedBy }
  * SendDmResult — { messageEventId, deliveries: List<DmDelivery> }
  * DmDelivery — { recipientNpub, recipientPubkeyHex, wrapId,
                   publishedTo, rejectedBy, relaySource }

All 19 verbs now registered in the generated dispatcher (15 read + 4
write). app_metadata.xml updated so Gemini's tool picker pitches the
broader surface, including the NIP-55 caveat.
2026-05-25 23:41:58 +00:00
Vitor PamplonaandGitHub e93492f491 Merge pull request #3047 from vitorpamplona/claude/zealous-mendel-TK91O
Phase 1: iOS support for Quartz and Commons (KMP purity)
2026-05-25 19:38:51 -04:00
Claude ac105ca2f3 chore(amethyst): enrich AppFunction outputs so Gemini can name names
Every verb that returned a pubkey now also returns the best-effort
display name from the local kind:0 cache. Before this commit Gemini
could only say "you got a DM from npub1abc…" — now it can say
"you got a DM from Alice" because the LLM has the field at hand
instead of having to chain another lookup.

  * NoteHit gains authorDisplayName (cache-resolved, null when the
    author's kind:0 isn't local yet). Applied to every verb that
    returns notes: searchNotes / getRecentFromFollows / getNotesByUser /
    searchByHashtag / getMyRecentNotes / getMyMentions /
    getRepliesToNote / searchArticles.

  * DmMessage gains fromDisplayName + sentByMe — the latter lets
    the caller distinguish "Alice said X" from "I said Y" when both
    appear in the same thread snapshot.

  * LiveStreamHit gains streamingUrl (was missing entirely — without
    it the verb is useless, you can't watch a stream you can't open)
    plus hostDisplayName.

  * getProfile cache-hit path now actually populates `about` — was
    silently null before because the early-return branch didn't read
    it out of UserInfo. Cache-miss path was always correct.

Implementation: one `displayNameOf(HexKey): String?` helper reads from
Amethyst.instance.cache (LocalCache) — the same cache the foreground
UI uses. Zero allocations beyond the lookup, no network round-trip.
2026-05-25 23:11:10 +00:00
Claude c10ed49631 feat(amethyst): Tier 2+3 read-only Gemini verbs — 15 total
Now exposing the full read-only Nostr surface to Gemini. Seven new
@AppFunction methods on top of the previous eight:

  * getMyRecentNotes(limit) — author=me filter on kind:1.

  * getMyMentions(limit) — p-tag=me filter on kind:1. "Did anyone @ me?".

  * getRepliesToNote(eventId, limit) — e-tag=eventId filter on kind:1.
    Pair with getMyRecentNotes(1) for "did anyone respond to my last post?".

  * getZapsReceived(hoursBack) — drains kind:9735 receipts addressed to
    the user in the window, parses the bolt11 invoice from each, sums
    sats. Returns total + zap count + unique zappers + count of
    receipts whose bolt11 was unparseable.

  * getRecentDms(peer?, hoursBack, limit) — NIP-17 gift-wrap drain +
    unwrapAndUnsealOrNull decrypt. kind:14 text DMs only for v1 (skip
    kind:15 encrypted-file headers to keep payloads bounded). Widens
    the `since` filter by 2 days for NIP-59's randomised-past
    created_at trick, then trims back to the requested window.

  * searchArticles(query, limit) — same as searchNotes but kind:30023
    long-form articles. Content snippet truncated at 2000 chars so a
    book-length article doesn't blow up the AppFunctions response;
    Gemini can ask the user whether to fetch the full article via a
    different verb.

  * getLiveStreams(limit) — NIP-53 kind:30311 with status=live (uses
    quartz's 8-hour staleness guard via LiveActivitiesEvent.isLive).
    Returns title, summary, host npub, start time, event id.

Plus updated res/xml/app_metadata.xml so Gemini's tool picker pitches
the full surface to users.

KSP-verified — 15 verbs total in the generated dispatcher:

    getActiveAccountInfo  getFollowing            getLiveStreams
    getMyMentions         getMyRecentNotes        getNotesByUser
    getProfile            getRecentDms            getRecentFromFollows
    getRepliesToNote      getZapsReceived         searchArticles
    searchByHashtag       searchNotes             searchProfiles

Write verbs (post, follow, zap, sendDm) still deferred behind the
signer-prompt plan in amethyst/plans/2026-05-25-appfunctions-signer-prompts.md
— no behavior change there.
2026-05-25 23:06:47 +00:00
Claude 5c46d0a7b3 feat(amethyst): five more read-only Gemini verbs — full Tier 1 read surface
After the on-device round-trip proved the AppFunctions plumbing works,
adding the verbs that make Gemini actually useful for a Nostr user.
All read-only, no signer interaction, all build on existing actions /
Account state.

  * getRecentFromFollows(limit) — "what's happening on Nostr today?"
    Drains recent kind:1 from people the user follows; same relay set
    the home-feed UI uses (account.homeRelays).

  * getNotesByUser(user, limit) — "what did Vitor post recently?"
    Accepts npub or 64-hex. Prefers the target's NIP-65 write relays
    when cached, falls back to the active account's home relays.

  * getProfile(user) — "who is npub1xq5...?". Cache-first via
    LocalCache; falls back to a short network drain for unseen users.
    Returns GetProfileResult{found, profile} so callers know whether
    the user just isn't in cache or doesn't have a kind:0 yet.

  * searchByHashtag(hashtag, limit) — "find Nostr posts about Bitcoin".
    NIP-12 `t` tag filter, lowercased to match the client convention.

  * getActiveAccountInfo() — "who am I logged in as?" Diagnostic verb
    returning npub, display name, follow count, outbox + DM relay
    counts. Distinguishes signed-in from signed-out via a flag rather
    than a magic empty result.

Plus:
  * decodeUserOrThrow helper for npub/hex parsing, throws
    AppFunctionInvalidArgumentException with a typed message so
    callers see "expected npub1… or 64-char hex" instead of a stack.
  * TextNoteEvent.toNoteHit helper — extracted from the existing
    searchNotes path to avoid duplication.
  * Updated res/xml/app_metadata.xml description so Gemini's tool
    picker can pitch a broader summary to the user.

KSP-verified: $AmethystAppFunctions_AppFunctionInvoker now dispatches
all eight verbs (the three from the previous commits plus these five).
2026-05-25 22:58:30 +00:00
Claude 0619788644 fix(amethyst): supply app_metadata so AppFunctions discovery actually works
After fixing the missing aggregated XML, Pixel 8 logcat still showed:
  D AppFunctions: Unable to resolve AppFunctionMetadata.

Comparing against Google's FilipFan/AppFunctionsPilot sample turned up
a separate metadata pointer the system requires:

  <property
      android:name="android.app.appfunctions.app_metadata"
      android:resource="@xml/app_metadata" />

This goes on the <application> element (not the service) and points to
an XML resource — distinct from the asset-side `app_functions.xml`
that the library auto-merges onto the service. The asset metadata
declares "here are my function ids and schemas"; the resource
metadata gives the agent a user-facing summary like "Search Nostr and
read your follows" to show users before they grant access.

Without the resource, the system can find our service and our
function list but can't resolve the descriptive metadata it shows
the user — so Gemini's tool picker stays empty.

Two new files:
  * amethyst/src/play/res/xml/app_metadata.xml — short description +
    displayDescription. Update when the @AppFunction surface grows.
  * play AndroidManifest <property> pointing at the resource.

Also dropped our explicit <service> declaration for
PlatformAppFunctionService — confirmed via the appfunctions-service
AAR that the library auto-merges that exact entry, complete with
permission + intent-filter, so our copy was redundant.
2026-05-25 22:03:46 +00:00
Claude 82188b719a fix(amethyst): generate app_functions.xml so the system can resolve metadata
The androidx.appfunctions-compiler runs in per-module mode by default,
emitting only the dispatcher Kotlin code. The aggregator that builds
the `app_functions.xml` + `app_functions_v2.xml` assets is gated behind
a KSP argument that was off.

Symptom on a Pixel 8 running our APK:
  D AppFunctions: Unable to resolve AppFunctionMetadata.

Without the aggregated asset, the manifest's
`android.app.appfunctions` property pointed at a file that didn't
exist; the System UI couldn't enumerate our @AppFunction methods so
Gemini's tool picker never saw them.

Setting `appfunctions:aggregateAppFunctions = "true"` on the amethyst
module turns the aggregator on. Verified post-build:
  assets/app_functions.xml           (688 bytes — manifest pointer + ids)
  assets/app_functions_v2.xml        (19.7 KB — full schemas + kdoc descriptions)
Both list searchProfiles / searchNotes / getFollowing with the kdoc
descriptions Gemini will render.

Library modules (commons/quartz) would set this to "false" — only the
final app emits the aggregate. We don't currently apply the KSP plugin
in any library module, so this is the only place that matters.
2026-05-25 21:50:56 +00:00
Claude 44aa262363 fix(commons): tighter Base64Image contract + pin serializer wire format
Two follow-up cleanups from the audit.

Base64Image.parse: when the regex matched but the data capture group
was missing, the migrated version returned an empty ByteArray. The
original threw NPE (java.util.Base64.getDecoder().decode(null)). Both
behaviors are accidents — restore the intended contract: throw the
existing "Unable to convert base64 to image" Exception explicitly.

FeedDefinitionSerializerTest gains a serializesToExpectedWireFormat
test that pins the byte-exact JSON output for a representative
multi-field feed. The legacy-Jackson migration claimed byte-identity
but only round-trip and reverse-compat were covered. Any future change
to field ordering / null handling / number formatting now fails this
test loudly, protecting users who have saved feeds on disk and any
downstream consumer expecting the stable order.
2026-05-25 20:05:00 +00:00
David KasparandGitHub 97bb6a5720 Merge pull request #3046 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-25 21:04:55 +02:00
Crowdin Bot 61372313fd New Crowdin translations by GitHub Action 2026-05-25 18:55:58 +00:00
davotoulaandClaude Opus 4.7 46c46aa69e i18n: translate missing cs/de/sv strings (apps, on-chain zap, legal, NIP-82)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:50:56 +02:00
nrobi144andClaude 64293f2fcc fix(desktop): fix text spacing in rich text viewer
Pure text paragraphs now render as a single Text composable instead
of individual words in FlowRow, eliminating unwanted inter-word gaps.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-25 09:50:43 +03:00
nrobi144andClaude d6875a144b feat(desktop): add @mention autocomplete to compose dialog
Type @ followed by a name in the compose/reply dialog to see a
dropdown of matching users from the local cache. Selecting a user
inserts their nostr:npub reference. Shows avatar + display name +
truncated npub.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-25 09:35:46 +03:00
nrobi144 76fdd5d25f Merge upstream/main into worktree-desktop-low-hanging-fruit 2026-05-25 06:45:44 +03:00
nrobi144andClaude 4d532e0698 fix(desktop): wire onHashtagClick, add RTL support, add identity fields
- Add onHashtagClick param to FeedNoteCard for hashtag navigation
- RTL paragraph alignment in DesktopRichTextViewer
- External identities (Twitter, GitHub, Mastodon) on profile card

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-25 06:43:21 +03:00
Claude d1749c314f fix: audit findings on iOS-readiness migration
Address bugs and gaps surfaced by an audit of the prior 14 commits.
JVM tests passed because of typealias / platform-type lenience that
won't hold on Native; these are real iOS compile / behavior issues.

BUG fixes (iOS compile failures):

- commons/.../Note.kt:899 — Iterable.sumOf { -> BigDecimal } is a
  JVM-stdlib-only overload. Common stdlib ships sumOf only for
  Int/Long/Double/Float/UInt/ULong. Replaced with fold(BigDecimal(0)).
- commons/.../Note.kt:889 — BigDecimal(it.event?.content): the quartz
  expect-class constructor takes String non-null; JVM accepted nullable
  via platform-type lenience and threw NPE caught downstream. Switched
  to ?.let { content -> BigDecimal(content) }.
- commons/.../Note.kt:838 — `catch (e: java.lang.Exception)` -> `Exception`.
- commons/.../feeds/custom/FeedDefinitionBuilder.kt + FeedBuilderState.kt:
  inline FQN `java.util.UUID.randomUUID().toString()` -> kotlin.uuid.Uuid.
  random().toString() (Kotlin 2.0+, @OptIn ExperimentalUuidApi).
  inline `System.currentTimeMillis() / 1000` -> TimeUtils.now() (already
  used elsewhere in the codebase).
- commons/.../viewmodels/NestViewModelTest.kt: moved from commonTest to
  jvmTest. The test imports NestViewModel + nestsclient, both of which
  the prior PR moved to jvmAndroid. commonTest depends on commonMain
  only, so the test would fail to compile for iosSimulatorArm64Test.

SUBTLE fixes:

- commons/.../UserRelaysCache.kt: the flow field used double-checked
  locking on a non-volatile var. JMM hazard on Native (ARM weak memory
  model) — outer fast-path could observe a partially-published
  WeakReference. Added @kotlin.concurrent.Volatile.
- commons/.../util/UrlValidation.ios.kt: NSURL.URLWithString("http:")
  returns non-null with scheme="http" and no host; JVM's URI.toURL()
  rejects with MalformedURLException. Reject scheme-only network URLs
  (http/https/ws/wss/ftp without a host) to match JVM behavior.
- commons/.../util/KmpLock.kt commonMain doc: corrected "NSLock" ->
  "NSRecursiveLock" to match the actual iOS implementation.

verifyKmpPurity gate extended (commons + quartz):

- Adds patterns: System.currentTimeMillis, Thread.sleep, java.util.UUID,
  kotlin.jvm.Synchronized, kotlin.jvm.Volatile.
- Each pattern paired with a hint pointing at the canonical KMP
  replacement; the error message surfaces both.
- Skips lines that start with //, *, or /* to avoid false positives on
  KDoc / migration notes.
2026-05-25 02:17:32 +00:00
Claude f1845d6a06 feat(commons): add iOS actuals for KmpLock, WeakReference, isValidUrl
Pre-stages the three iosMain actuals that the macOS CI run is most
likely to demand once it compiles :commons for Native (the dev
container can't extract the K/N LLVM toolchain to validate locally).

- KmpLock.ios.kt: NSRecursiveLock — mirrors the ReentrantLock
  semantics the jvmAndroid actual exposes (reentrant per-thread).
- WeakReference.ios.kt: actual typealias to kotlin.native.ref.
  WeakReference<T> — same constructor + get(): T? shape as the
  jvmAndroid typealias to java.lang.ref.WeakReference<T>.
- UrlValidation.ios.kt: NSURL.URLWithString with an explicit scheme
  check, since NSURL is more permissive than JVM's URI.toURL() and
  accepts scheme-less relatives that the JVM contract rejects.

Lands together so the next CI run's failure mode (if any) is more
informative than "iosArm64 unresolved reference" three times over.
2026-05-25 00:44:38 +00:00