- Correct the AUTH-approval persistence check: this JVM uses MacOSXPreferences (~/Library/Preferences/com.vitorpamplona.amethyst.plist), not ~/.java/.userPrefs. Updated T3.b/c/d to read it via plutil. - Record session results (T1,T2,T3a,T3c,T6,T6b,T8,T12 pass) and the three bugs found+fixed during the run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 KiB
Desktop DM Reliability — Testing Playbook
Branch: feat/desktop-dm-reliability rebased onto upstream/main
Tester: _______________________
Date: _______________________
Instructions: Follow this top to bottom. Every step is an action or an observation. Don't skip ahead — later tests assume state from earlier ones. Total ≈ 40 min for full pass.
Session results — 2026-07-09 (live run)
| Test | Result | Notes |
|---|---|---|
| T1 startup / AUTH wired | ✅ PASS | both Init, Subscribe + AUTH wired logged; no CME crash |
| T2 tier-1 self-DM (no banner) | ✅ PASS | published, no PendingAuthApproval prompt |
| T3.a tier-2 banner render | ✅ PASS | relay.ditto.pub banner, icon clear of traffic lights, 3 buttons |
T3.c Always persists |
✅ PASS | auth/<pubkey>/ wss://relay.ditto.pub/ = ALWAYS in the plist |
| T6 no-10050 blocks send | ✅ PASS | "Recipient has no DM relay list", send disabled |
| T6b kind:10002-only blocks | ✅ PASS | same block; zero publish to the NIP-65 read relay |
T8 wrap p-tag relay hint |
✅ PASS (after fix) | 3-element ["p", hex, wss://nos.lol/] on the wrap |
T12 kind:1059 sub has no since |
✅ PASS | since removed from giftWrapsToMe signature |
T3.b Once / T3.d Never |
⏭️ not run | logic covered by AuthApprovalEndToEndTest |
| T9 group rumor.id | ⏭️ covered-by-construction | rumor signed once before the per-recipient loop |
| T10 AUTH-under-load / T11 bunker | ⏭️ not run | no challenging relay / bunker on hand |
Bugs found & fixed this run:
DmInboxRelayResolverLocalCache fast-path used lenientdmInboxRelays()→ NIP-65 read-relay leak. NowdmInboxRelaysStrict().NewDmDialogrendered pasted npubs of metadata-less users as non-clickable → couldn't start a DM by npub. NowgetOrCreateUser.- NIP-17
p-tag relay hint was plumbed in quartz but never passed byDesktopIAccount→ every wrap shipped a 2-elementptag. Now wired.
Setup (once, ~3 min)
1. In a terminal, cd to the worktree and confirm you're on the right commit:
cd /path/to/AmethystMultiplatform/.worktrees/feat/desktop-dm-reliability
git rev-parse HEAD
- Expect:
fcfc43eb44(or later). If different:git pulland re-verify.
2. Wipe any prior AUTH grants so persistence tests start clean:
rm -rf ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth
3. Launch the app (keep this terminal visible — we'll read logs from it):
./gradlew :desktopApp:run
- Expect: window appears in 15–30 s (cold) / 5 s (warm).
4. In the app, log in with your primary account. Call this User A.
- Expect: sidebar loads, feed populates.
5. In the terminal, look for these two lines (they appear within 5 s of login):
[RelayAuthenticator] Init, Subscribe
[DesktopAuthCoordinator] AUTH wired for <pubkey8>
- If both appear: ✅ setup complete. Proceed to T1.
- If either is missing: STOP. Tell me the terminal output.
T1 — Startup smoke check (already ✅ during setup)
Nothing extra to do — the two log lines above ARE T1.
- T1 PASS — both
Init, SubscribeandAUTH wired for <pubkey8>printed with no exceptions
T2 — Tier-1 self-DM (no banner) — 2 min
Goal: verify your own DM-inbox relays auto-AUTH silently.
Steps:
1. In the sidebar, click Chats (chat bubble icon).
2. At the top of the conversation list, click the + icon (new conversation).
3. Paste your OWN npub into the recipient field. Confirm.
4. In the message box, type: t1 self-dm test
5. Watch the top of the content area (where the yellow banner would appear).
- Expected: send button enables blue → no yellow AUTH banner appears anywhere.
6. Click the send arrow (right side of the message box).
7. Wait 3 s. The message should appear in your inbox.
- Expected: message appears in the conversation. Terminal has NO
AuthApprovalPolicyprompt lines.
Record:
- T2.1 No AUTH banner appeared: YES / NO
- T2.2 Message arrived: YES / NO
- T2 PASS — both YES
T3 — Tier-2 banner + persistence — 8 min
Goal: trigger a challenge from an AUTH-required relay NOT in your kind:10050, verify the banner renders + all three buttons persist correctly.
T3.a — Trigger the banner
1. Open Settings. (Look for a gear/cog icon in the sidebar. If absent, try the app menu → Settings.)
2. Go to the Relays tab.
3. Find the "Add relay" input. Paste: wss://pyramid.fiatjaf.com
4. Save/apply (button label varies — usually "Add" or "Save").
5. Wait 1–3 s. Watch the top of the content area (below the title bar, above the main content).
- Expected: a yellow-tinted horizontal row slides in showing:
- Lock icon on the left (with proper margin from window edge — 80dp — not overlapping the traffic lights)
pyramid.fiatjaf.comin a semi-bold heading- Subtext: "requires authentication to deliver this message"
- Three buttons on the right:
OnceAlwaysNever
Record:
- T3.a.1 Banner appeared within 3 s: YES / NO
- T3.a.2 Icon + text NOT overlapping traffic lights: YES / NO
- T3.a.3 All three buttons visible: YES / NO
T3.b — [Once] behaviour (session-only, no persistence)
6. Click Once.
- Expected: banner slides away immediately, no visible change to relay state.
7. In a second terminal, check the Preferences store did NOT get written for this relay.
Prefs location (macOS). This JVM uses the
MacOSXPreferencesbacking store, NOT~/.java/.userPrefs. Java prefs land in~/Library/Preferences/com.vitorpamplona.amethyst.plistunder anauth/<full-pubkey>/node. Read it withplutil:
plutil -convert xml1 -o - ~/Library/Preferences/com.vitorpamplona.amethyst.plist | grep -i "pyramid\|ditto"
- Expected: empty output (ONCE is not persisted).
8. Close the app (Cmd+Q). Wait 2 s. Relaunch via ./gradlew :desktopApp:run. Log in as A again.
9. Wait ~5 s. The banner for pyramid.fiatjaf.com should reappear (session state was not saved).
Record:
- T3.b.1 After
[Once]: Preferences NOT written: YES / NO - T3.b.2 After restart: banner reappeared: YES / NO
T3.c — [Always] behaviour (persisted grant)
10. In the banner that just reappeared, click Always.
- Expected: banner slides away,
pyramid.fiatjaf.comnow shows "Authenticated" in Settings → Relays.
11. Check Preferences was written:
plutil -convert xml1 -o - ~/Library/Preferences/com.vitorpamplona.amethyst.plist | grep -i "pyramid\|ditto\|ALWAYS"
- Expected: the relay URL (e.g.
wss://relay.ditto.pub/) followed byALWAYS, under theauth/<full-pubkey>/node.
12. Close app (Cmd+Q). Relaunch. Log in as A.
- Expected: relay auto-authenticates in the background. No banner appears for
pyramid.fiatjaf.com.
Record:
- T3.c.1 Preferences shows
ALWAYS: YES / NO - T3.c.2 After restart: no banner, auto-authenticated: YES / NO
T3.d — [Never] behaviour (persisted block)
13. Add a different AUTH-required relay. If you have another, use it. Otherwise, try wss://nostr.wine (they AUTH-challenge non-subscribers) or wss://relay.snort.social.
14. Wait for the new banner to appear.
15. Click Never.
- Expected: banner disappears. The relay shows as connected but "not authenticated".
16. Check Preferences:
plutil -convert xml1 -o - ~/Library/Preferences/com.vitorpamplona.amethyst.plist | grep -i "<relay-domain>\|BLOCKED"
- Expected: the relay URL followed by
BLOCKED, under theauth/<full-pubkey>/node.
17. Restart the app. Log in as A.
- Expected: the BLOCKED relay never surfaces a banner. It stays "not authenticated". No
kind:22242AUTH event ever sent to it.
Record:
- T3.d.1 Preferences shows
BLOCKED: YES / NO - T3.d.2 After restart: no banner, no AUTH sent: YES / NO
T3 sign-off: Complete T3.a, T3.b, T3.c, T3.d.
- T3 PASS — all four subs green
T4 — Multiple concurrent banners — 3 min
Goal: verify multiple pending banners stack correctly and resolve independently.
Steps:
1. In Settings → Relays, quickly add 3 different AUTH-required relays back-to-back. Suggested set:
wss://pyramid.fiatjaf.com(if not already blocked/allowed)wss://relay.nostr.com.auwss://nostr.wine
2. Watch the banner area — all 3 rows should appear stacked vertically within ~3 s.
3. Click Once on the middle row.
- Expected: ONLY the middle row disappears. The other two remain visible.
4. (Optional stress test) Add 5+ more AUTH-required relays.
- Expected: first 3 shown inline; row at the bottom reads "+N more relays pending approval".
Record:
- T4.1 3 banners stack vertically: YES / NO
- T4.2 Middle-row dismiss only affects itself: YES / NO
- T4.3 "+N more" row shows when >3 pending: YES / NO
- T4 PASS — all three YES
T5 — Per-account isolation — 4 min
Goal: verify AUTH grants are scoped per-account and cleaned on logout.
Steps:
1. Ensure A has at least one ALWAYS grant (from T3.c: pyramid.fiatjaf.com).
2. Log out of A (sidebar → profile → Logout, or app menu).
- Terminal: watch for
DesktopAuthCoordinatorteardown lines (no exceptions).
3. Log in as User B (different pubkey — nsec, npub, or bunker).
- Terminal: expect
[DesktopAuthCoordinator] AUTH wired for <B-pubkey8>— different from A's.
4. Add wss://pyramid.fiatjaf.com in Settings → Relays for B.
- Expected: banner appears (B does NOT inherit A's
ALWAYSgrant).
5. Check that A's and B's Preferences are separate:
ls ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth/
- Expected: two directories, one per full pubkey.
6. Click Always on B's banner.
7. Log out of B. Log back into A.
- Terminal:
AUTH wired for <A-pubkey8>again.
8. Watch for banners.
- Expected: no banner for
pyramid.fiatjaf.com(A'sALWAYSstill persisted).
Record:
- T5.1 Coordinator teardown clean on logout (no exceptions): YES / NO
- T5.2 B sees banner for A-approved relay (isolation): YES / NO
- T5.3 Two separate Preferences dirs exist: YES / NO
- T5.4 Re-login to A: no re-prompt: YES / NO
- T5 PASS — all four YES
T6 — P0 security fix (no-10050 recipient) — 5 min
Goal: verify DMs are NOT silently broadcast to your general relays when the recipient has no kind:10050.
T6.a — Create a "no-inbox" test recipient
1. In a terminal, generate a fresh nsec/npub pair:
# Option: use nak
nak key generate
# Copy the printed nsec and npub
Or use any known npub of an account that never published kind:10050.
2. In Amethyst as A, click + in Chats. Paste the test npub. Confirm.
T6.b — Verify the UI blocks send
3. Type any message.
4. Look at the row below the message input.
- Expected: red text "Recipient has no DM relay list — messages cannot be delivered"
- Expected: send button is grey/disabled.
5. Try clicking send anyway.
- Expected: nothing happens (button disabled). Or, if enabled by upstream UI quirk,
DmSendTrackershows "No relays available" briefly.
T6.c — Verify no wrap leaves the app (optional, for the security-conscious)
6. In a terminal, run:
sudo tcpdump -i any -A -s 0 'tcp port 443 or tcp port 80' 2>/dev/null | grep -i "kind\":1059"
7. In the app, try to send. Watch the tcpdump output for 30 s.
- Expected: zero output. No gift wrap (kind 1059) publishes anywhere.
8. Stop tcpdump with Ctrl+C.
Record:
- T6.1 UI shows "no DM relay list" warning: YES / NO
- T6.2 Send button disabled: YES / NO
- T6.3 No
kind":1059in outgoing traffic during send attempt: YES / NO / SKIPPED - T6 PASS — T6.1 and T6.2 both YES (T6.3 optional but recommended)
T6b — Strict kind:10050 (NIP-65 read-relay non-leak) — 4 min
Goal: verify the review fix — a recipient that DOES publish NIP-65 read
relays (kind:10002) but has NO kind:10050 is still treated as unreachable.
The lenient fast-path bug would have published the wrap to those NIP-65 read
relays; the fix must NOT.
T6b.a — Create a recipient with kind:10002 but no kind:10050
1. Generate a fresh key and publish ONLY a NIP-65 relay list (no 10050):
nak key generate # copy nsec + npub
# publish a kind:10002 with a read relay, and NO kind:10050:
echo '{"kind":10002,"tags":[["r","wss://relay.damus.io","read"]],"content":""}' \
| nak event --sec <nsec> wss://relay.damus.io wss://nos.lol
2. As User A, open a new chat to that npub so A's LocalCache ingests the recipient's kind:10002 (send/hover the profile so the relay list loads).
T6b.b — Verify send is blocked, not routed to the read relay
3. Type a message. Observe the row under the input.
- Expected: same "no DM relay list — messages cannot be delivered" warning as T6; send disabled.
- Wrong (pre-fix bug): send is ENABLED and the wrap goes to
wss://relay.damus.io(the recipient's NIP-65 read relay).
4. (Optional, definitive) tcpdump as in T6.c while attempting send.
- Expected: zero
kind":1059frames to the recipient's kind:10002 relays.
Record:
- T6b.1 Send blocked despite recipient having kind:10002: YES / NO
- T6b.2 No wrap sent to NIP-65 read relay (if tcpdump run): YES / NO / SKIPPED
- T6b PASS — T6b.1 YES
T7 — Indexer fan-out + F-01 unauth check — 6 min
Goal: verify the resolver probes indexer relays with an UNAUTHENTICATED client (no kind:22242 AUTH events leaked to indexers).
T7.a — Prime the state
1. Restart the app (Cmd+Q, then ./gradlew :desktopApp:run).
- Fresh LocalCache = maximum chance the resolver actually fires.
2. Log in as A.
T7.b — Set up traffic capture (optional but revealing)
3. In a second terminal, start capturing all WebSocket traffic:
sudo tshark -i any -Y 'websocket' -T fields -e ws.payload 2>/dev/null | head -c 100000
Or (simpler):
sudo tcpdump -i any -A -s 0 'tcp port 443' 2>/dev/null > /tmp/dm-traffic.log &
T7.c — Trigger the resolver
4. Pick a recipient who HAS a kind:10050 published (a NIP-17-active account) but whom you have NEVER DM'd from account A.
5. In Amethyst, click + in Chats. Paste the recipient's npub. Confirm.
6. Watch the pre-send row.
- Expected sequence:
- Initial: red "no DM relay list" warning (LocalCache miss).
- Within 2–5 s: warning disappears (resolver probe found the recipient's
kind:10050on an indexer). - Send button turns blue.
T7.d — Verify F-01 (no AUTH to indexer)
7. Search the captured traffic for kind:22242 AUTH events:
grep -i "\"kind\":22242" /tmp/dm-traffic.log | head -20
- Expected: any
kind:22242events found should only be to relays in your existing DM-inbox set — NOT to the indexer set (relay.nos.social,relay.damus.io,nos.lol,relay.nostr.band,purplerelay.com).
8. In the app, type a message and send.
- Expected: send succeeds. Recipient's actual DM-inbox relay receives the wrap.
9. Stop tcpdump: sudo pkill tcpdump
T7.e — Verify LRU cache hit on second send
10. Immediately compose a second DM to the same recipient. Send.
- Expected: send is immediate, no delay. Resolver hits its LRU cache, no new indexer probe.
Record:
- T7.1 Warning cleared within 5 s (resolver probe worked): YES / NO
- T7.2 Send button became enabled after probe: YES / NO
- T7.3 No
kind:22242AUTH sent to indexer relays: YES / NO / SKIPPED - T7.4 DM delivered to recipient: YES / NO
- T7.5 Second DM to same recipient: no probe delay: YES / NO
- T7 PASS — T7.1, T7.2, T7.4, T7.5 all YES
T8 — Wrap p-tag relay hint — 4 min
Goal: verify the outgoing gift wrap includes the recipient's primary DM relay as the third element of the p tag.
Steps:
1. Send a DM to any recipient with a known kind:10050 (e.g. the one from T7).
2. In a terminal, use nak (or websocat) to query one of the recipient's DM-inbox relays for their gift wraps:
RECIPIENT_HEX=<paste-recipient-hex-pubkey>
DM_RELAY=<paste-one-of-their-10050-relays>
nak req -k 1059 --tag "p=$RECIPIENT_HEX" "$DM_RELAY" | head -5
3. Find the wrap you just sent (highest created_at). Look at its p tag.
- Expected:
["p", "<recipient-hex>", "wss://recipient-primary-relay/"]— 3 elements, third is a valid relay URL.
4. For contrast, send a DM to a recipient whose kind:10050 you have NO indexer/cache hit for (e.g. the one from T6 if you have their nsec to simulate — otherwise skip).
- Expected: wrap's
ptag has only 2 elements:["p", "<hex>"]— no fake empty third element.
Record:
- T8.1 With known relay: 3-element
ptag: YES / NO - T8.2 Without known relay: 2-element
ptag (no empty third): YES / NO / SKIPPED - T8 PASS — T8.1 YES
T9 — Group DM shared rumor.id — 5 min
Goal: verify all recipient wraps in a group DM decrypt to a rumor with the SAME id.
Steps:
1. In Amethyst as A, click + in Chats. Add 3 recipient npubs (you can include yourself as one, plus 2 others whose kind:10050 is known).
2. Type a distinctive message: t9 group rumor coherence test. Send.
3. For each recipient, use nak to fetch the gift wrap from their DM-inbox relay:
for RECIPIENT in $RECIPIENT_A $RECIPIENT_B $RECIPIENT_C; do
nak req -k 1059 --tag "p=$RECIPIENT" wss://relay.example/ | head -3
done
4. Ideally decrypt each wrap (requires each recipient's nsec). But since all 3 seals encode the same rumor, the rumor id should be identical across the 3 wraps.
5. If you have at least 2 recipient nsecs, decrypt via nak:
nak decrypt --sec $NSEC "<encrypted-wrap-content>"
# Look at the inner rumor's "id" field
6. Compare the rumor id across the wraps.
- Expected: all 3 rumor
ids are IDENTICAL.
7. (Bonus) Have one of the recipients (in another Amethyst instance or via nak) react to the message with +.
8. Confirm A and other recipients see the reaction.
- Expected: reaction targets the shared
rumor.idand appears cross-recipient.
Record:
- T9.1 All wraps decrypt to same rumor.id: YES / NO / SKIPPED (needs multi-account decrypt)
- T9.2 Cross-recipient reaction visible: YES / NO / SKIPPED
- T9 PASS — T9.1 YES (or explicitly skipped)
T10 — Outbox AUTH carve-out under load — 4 min
Goal: verify multiple queued events are NOT silently dropped during AUTH negotiation.
Steps:
1. In Settings → Relays, ensure you have wss://pyramid.fiatjaf.com connected. If you [Always]-approved it in T3.c, first log out and back in so the relay reconnects and re-challenges.
2. In the compose dialog, publish 5 notes rapidly (10 seconds apart is fine):
t10 note 1
t10 note 2
t10 note 3
t10 note 4
t10 note 5
3. In the terminal, watch for AUTH activity on pyramid.fiatjaf.com:
[RelayAuthenticator] ... auth-required: ...
[RelayAuthenticator] ... AUTH accepted ...
4. Once AUTH completes, all 5 notes should publish to pyramid.fiatjaf.com.
5. Verify by querying pyramid.fiatjaf.com for your recent notes:
nak req -a $A_HEX -k 1 wss://pyramid.fiatjaf.com | head -10
- Expected: all 5
t10 note Nevents present onpyramid.fiatjaf.com.
Record:
- T10.1 All 5 notes visible on
pyramid.fiatjaf.com: YES / NO - T10.2 Terminal shows AUTH succeeded before drops: YES / NO
- T10 PASS — T10.1 YES
T11 — Bunker Semaphore(4) (bunker users only) — 3 min
Skip if you don't have a NIP-46 bunker (nsec.app / Amber).
Goal: verify NIP-17 group DMs are rate-limited to ≤4 concurrent bunker RPCs.
Steps:
1. Log out. Log in with a bunker (bunker:// URI).
2. Compose a group DM to 5 recipients (5 different npubs with known kind:10050).
3. In nsec.app / Amber, watch the request feed as you click Send.
- Expected: at most 4 requests in-flight at any moment. Requests process in batches of 4.
4. For comparison: log out, log back in with a local nsec. Repeat the 5-recipient send.
- Expected: local signer runs all 5 requests in parallel (no Semaphore cap).
Record:
- T11.1 Bunker: ≤4 concurrent RPCs: YES / NO / SKIPPED (no bunker)
- T11.2 Local: fully parallel: YES / NO / SKIPPED
- T11 PASS — either both YES or both SKIPPED
T12 — kind:1059 subscription has no since — 3 min
Goal: verify the outgoing REQ for gift wraps has NO since filter (would silently drop old-timestamped wraps).
Steps:
1. In one terminal, run a WebSocket relay proxy that echoes traffic (or use nak to inspect):
# Simplest: read the desktop subscription code directly
grep -n "FilterDMs.giftWrapsToMe\|since" desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt
- Expected: signature
fun giftWrapsToMe(userPubKeyHex: HexKey)— NOsinceparameter.
2. Live check (harder but definitive): use mitmproxy or websocat in proxy mode to intercept WebSocket traffic from the app.
3. Alternatively: rely on the unit tests. Confirm they pass:
./gradlew :quartz:jvmTest --tests "com.vitorpamplona.quartz.nip59Giftwrap.wraps.*"
- Expected: BUILD SUCCESSFUL.
Record:
- T12.1
giftWrapsToMesignature has nosince: YES / NO - T12.2 Unit tests pass: YES / NO
- T12 PASS — both YES
T13 — Pre-send alignment + resolver probe (verified working) — sanity re-check, 3 min
Already confirmed during the pre-launch fix. Quick re-check:
Steps:
1. In Amethyst as A, open a fresh DM with a recipient whose kind:10050 is NOT in your LocalCache (e.g. a fresh contact — click a new profile, then compose DM).
2. Watch the pre-send row.
- Expected: red "no DM relay list" warning appears initially.
3. Wait 2–5 s.
- Expected: warning clears on its own (resolver probe found the recipient via indexer). Send button turns blue.
Record:
- T13.1 Warning appears initially: YES / NO
- T13.2 Warning clears within 5 s (resolver worked): YES / NO
- T13 PASS — both YES
Sign-off
| Test | Pass? | Notes |
|---|---|---|
| Setup | ⬜ | |
| T1 startup wiring | ⬜ | |
| T2 tier-1 self-DM | ⬜ | |
| T3 tier-2 banner (T3.a–T3.d) | ⬜ | |
| T4 multiple banners | ⬜ | |
| T5 per-account isolation | ⬜ | |
| T6 P0 SECURITY | ⬜ | Highest priority |
| T7 F-01 unauth indexer | ⬜ | Highest priority |
| T8 wrap p-tag relay hint | ⬜ | |
| T9 group DM rumor id | ⬜ | |
| T10 outbox AUTH carve-out | ⬜ | |
| T11 bunker Semaphore | ⬜ | Skip if no bunker |
| T12 no since filter | ⬜ | |
| T13 pre-send alignment | ⬜ |
Overall: ⬜ PASS — ready for PR / ⬜ FAIL — see blockers / ⬜ NEEDS REVISIT
Blockers: _______________________________________________________
Tester signature: _______________________ Date: _______________________
Known pre-existing issues (NOT branch regressions)
ConcurrentModificationExceptionatRelayLatencyTracker.sweep:182during rapid account switching. Kills UI thread; coroutines keep running. Documented in memorydesktop_relay_health_cme_crash.NoClassDefFoundErrorforCompressionQualityon stale gradle daemon. Fix:./gradlew --stop && ./gradlew :desktopApp:run.
Known non-issues (don't file as bugs)
[GiftWrapEvent] Couldn't Decrypt the content …debug lines — normal LocalCache trial-decrypt for wraps not addressed to you.- VLC
securetransport tls client error— pre-existing media playback warnings. [NIP19 Parser] Issue trying to Decode NIP19 …— pre-existing, malformed identifiers in some events.DmBroadcastBanner(send-progress) may render simultaneously withAuthApprovalBanner— distinguish by buttons: AUTH banner hasOnce/Always/Never; broadcast has send-count status.
Out of scope for this branch
Explicit follow-ups per the deepening synthesis:
- Persistent retry queue with exp-backoff (SQLite-backed)
- Per-message delivery state in bubbles (
✓✓✓⟳⚠) - Bunker progress UI wiring
- Window-focus re-AUTH
- NIP-46 batch
get_conversation_keysRPC - Android UI parity for AUTH banner
- Manual relay-entry dialog when recipient has no 10050
- NIP-09 deletion of self-copies on kind:10050 rotation
- Fix for pre-existing
RelayLatencyTracker.sweepCME