Files
amethyst/docs/plans/2026-06-17-desktop-dm-reliability-manual-testing.md
T
nrobi144andClaude Opus 4.8 fe96311d93 test: record 2026-07-09 live run results + fix macOS prefs path in sheet
- 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>
2026-07-09 15:43:03 +03:00

25 KiB
Raw Blame History

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:

  1. DmInboxRelayResolver LocalCache fast-path used lenient dmInboxRelays() → NIP-65 read-relay leak. Now dmInboxRelaysStrict().
  2. NewDmDialog rendered pasted npubs of metadata-less users as non-clickable → couldn't start a DM by npub. Now getOrCreateUser.
  3. NIP-17 p-tag relay hint was plumbed in quartz but never passed by DesktopIAccount → every wrap shipped a 2-element p tag. 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 pull and 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, Subscribe and AUTH 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 AuthApprovalPolicy prompt 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.com in a semi-bold heading
    • Subtext: "requires authentication to deliver this message"
    • Three buttons on the right: Once Always Never

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 MacOSXPreferences backing store, NOT ~/.java/.userPrefs. Java prefs land in ~/Library/Preferences/com.vitorpamplona.amethyst.plist under an auth/<full-pubkey>/ node. Read it with plutil:

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.com now 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 by ALWAYS, under the auth/<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 the auth/<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:22242 AUTH 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.au
  • wss://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 DesktopAuthCoordinator teardown 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 ALWAYS grant).

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's ALWAYS still 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, DmSendTracker shows "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":1059 in 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":1059 frames 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:10050 on 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:22242 events 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:22242 AUTH 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 p tag has only 2 elements: ["p", "<hex>"] — no fake empty third element.

Record:

  • T8.1 With known relay: 3-element p tag: YES / NO
  • T8.2 Without known relay: 2-element p tag (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.id and 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 N events present on pyramid.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) — NO since parameter.

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 giftWrapsToMe signature has no since: 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)

  • ConcurrentModificationException at RelayLatencyTracker.sweep:182 during rapid account switching. Kills UI thread; coroutines keep running. Documented in memory desktop_relay_health_cme_crash.
  • NoClassDefFoundError for CompressionQuality on 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 with AuthApprovalBanner — distinguish by buttons: AUTH banner has Once/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_keys RPC
  • 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.sweep CME