From ad7aa5a211e32bedc5ac91c5d7111f8dfd6adb12 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Jan 2026 08:08:40 +0000 Subject: [PATCH] fix(nip-22): improve relay selection strategy to fix root not found errors Problem: NIP-22 adapter was struggling to find comment root events, showing "Comment root not found" errors frequently. Root cause: Insufficient relay coverage when fetching root events. We were only checking a few relays (pointer hints + root author's outbox), but the root event might be on relays that only the comment author knows about. Solution: Multi-source relay aggregation with better prioritization: 1. Pointer hints (from nevent/naddr encoding) 2. Comment tag hints (relay URLs from A/E tags per NIP-22) 3. Comment author's outbox relays (CRITICAL - they saw the root somewhere!) 4. Root author's outbox relays (where they published to) 5. Aggregator relays (fallback for discovery) Key improvements: - Added comment author's outbox relay fetching (they definitely saw the root) - Increased relay limits from 3 to 5 per source for better coverage - Always include aggregator relays as fallback - Added detailed console logging for debugging relay selection - Check EventStore cache first in fetch methods to avoid redundant queries This aggressive multi-source strategy should dramatically reduce "root not found" errors by ensuring we query all possible relay sources. All 1114 tests passing. --- src/lib/chat/adapters/nip-22-adapter.ts | 139 ++++++++++++++++++++---- 1 file changed, 117 insertions(+), 22 deletions(-) diff --git a/src/lib/chat/adapters/nip-22-adapter.ts b/src/lib/chat/adapters/nip-22-adapter.ts index 425aaaa..5eb1635 100644 --- a/src/lib/chat/adapters/nip-22-adapter.ts +++ b/src/lib/chat/adapters/nip-22-adapter.ts @@ -910,21 +910,49 @@ export class Nip22Adapter extends ChatProtocolAdapter { /** * Helper: Fetch event by EventPointer with improved relay selection - * Uses: relay hints from pointer, relay hints from comment tags, author's outbox relays + * Uses: relay hints from pointer, relay hints from comment tags, + * comment author's outbox relays (they saw the root somewhere), + * root author's outbox relays, aggregator relays */ private async fetchEventByPointer( pointer: EventPointer, additionalHints: string[] = [], commentEvent?: NostrEvent, ): Promise { - // Merge relay hints from multiple sources + // Check EventStore first + const cached = await firstValueFrom(eventStore.event(pointer.id), { + defaultValue: undefined, + }); + if (cached) return cached; + + console.log( + `[NIP-22] Fetching root event ${pointer.id.slice(0, 8)} for comment ${commentEvent?.id.slice(0, 8) || "unknown"}`, + ); + + // Gather relay hints from all available sources const pointerHints = pointer.relays || []; const commentHints = commentEvent ? this.extractRelayHintsFromComment(commentEvent, pointer.id) : []; - // Get author's outbox relays if we know the author - let authorOutbox: string[] = []; + // Get comment author's outbox relays (they saw the root event somewhere) + let commentAuthorOutbox: string[] = []; + if (commentEvent) { + try { + const relayListEvent = await firstValueFrom( + eventStore.replaceable(10002, commentEvent.pubkey, ""), + { defaultValue: undefined }, + ); + if (relayListEvent) { + commentAuthorOutbox = getOutboxes(relayListEvent); + } + } catch { + // Ignore errors + } + } + + // Get root author's outbox relays if we know the author + let rootAuthorOutbox: string[] = []; if (pointer.author) { try { const relayListEvent = await firstValueFrom( @@ -932,27 +960,62 @@ export class Nip22Adapter extends ChatProtocolAdapter { { defaultValue: undefined }, ); if (relayListEvent) { - authorOutbox = getOutboxes(relayListEvent); + rootAuthorOutbox = getOutboxes(relayListEvent); } } catch { - // Ignore errors fetching relay list + // Ignore errors } } - // Merge all hints (prioritize pointer hints, then comment hints, then author outbox) + // Merge all hints with priority: + // 1. Pointer hints (explicit in nevent) + // 2. Comment tag hints (relay where comment saw the root) + // 3. Comment author outbox (where they read from) + // 4. Root author outbox (where they published to) + // 5. Additional hints + // 6. Aggregator relays (fallback) const allHints = mergeRelaySets( pointerHints, commentHints, - authorOutbox.slice(0, 3), // Limit outbox to 3 relays + commentAuthorOutbox.slice(0, 5), // More relays from comment author + rootAuthorOutbox.slice(0, 5), // More relays from root author additionalHints, + AGGREGATOR_RELAYS, // Always include aggregators as fallback ); - return this.fetchEvent({ id: pointer.id, kind: pointer.kind }, allHints); + console.log( + `[NIP-22] Querying ${allHints.length} relays for root event (pointer=${pointerHints.length}, comment=${commentHints.length}, commentAuthor=${commentAuthorOutbox.length}, rootAuthor=${rootAuthorOutbox.length}, agg=${AGGREGATOR_RELAYS.length})`, + ); + + const filter: Filter = { + ids: [pointer.id], + limit: 1, + }; + + if (pointer.kind !== undefined) { + filter.kinds = [pointer.kind]; + } + + const events = await firstValueFrom( + pool.request(allHints, [filter], { eventStore }).pipe(toArray()), + ); + + const found = events[0] || null; + if (found) { + console.log(`[NIP-22] Found root event ${pointer.id.slice(0, 8)}`); + } else { + console.warn( + `[NIP-22] Root event ${pointer.id.slice(0, 8)} not found on any of ${allHints.length} relays`, + ); + } + + return found; } /** * Helper: Fetch addressable event by AddressPointer with improved relay selection - * Uses: relay hints from pointer, relay hints from comment tags, author's outbox relays + * Uses: relay hints from pointer, relay hints from comment tags, + * comment author's outbox relays, root author's outbox relays, aggregator relays */ private async fetchAddressableEvent( pointer: AddressPointer, @@ -968,37 +1031,60 @@ export class Nip22Adapter extends ChatProtocolAdapter { ); if (cached) return cached; - // Merge relay hints from multiple sources - const pointerHints = pointer.relays || []; const coordinate = `${kind}:${pubkey}:${identifier}`; + console.log( + `[NIP-22] Fetching addressable root ${coordinate} for comment ${commentEvent?.id.slice(0, 8) || "unknown"}`, + ); + + // Gather relay hints from all available sources + const pointerHints = pointer.relays || []; const commentHints = commentEvent ? this.extractRelayHintsFromComment(commentEvent, undefined, coordinate) : []; - // Get author's outbox relays - let authorOutbox: string[] = []; + // Get comment author's outbox relays (they saw the root event somewhere) + let commentAuthorOutbox: string[] = []; + if (commentEvent) { + try { + const relayListEvent = await firstValueFrom( + eventStore.replaceable(10002, commentEvent.pubkey, ""), + { defaultValue: undefined }, + ); + if (relayListEvent) { + commentAuthorOutbox = getOutboxes(relayListEvent); + } + } catch { + // Ignore errors + } + } + + // Get root author's outbox relays + let rootAuthorOutbox: string[] = []; try { const relayListEvent = await firstValueFrom( eventStore.replaceable(10002, pubkey, ""), { defaultValue: undefined }, ); if (relayListEvent) { - authorOutbox = getOutboxes(relayListEvent); + rootAuthorOutbox = getOutboxes(relayListEvent); } } catch { - // Ignore errors fetching relay list + // Ignore errors } - // Merge all hints (prioritize pointer hints, then comment hints, then author outbox) + // Merge all hints with priority (same as fetchEventByPointer) const allHints = mergeRelaySets( pointerHints, commentHints, - authorOutbox.slice(0, 3), // Limit outbox to 3 relays + commentAuthorOutbox.slice(0, 5), + rootAuthorOutbox.slice(0, 5), additionalHints, + AGGREGATOR_RELAYS, ); - const relays = - allHints.length > 0 ? allHints : await this.getDefaultRelays(); + console.log( + `[NIP-22] Querying ${allHints.length} relays for addressable root (pointer=${pointerHints.length}, comment=${commentHints.length}, commentAuthor=${commentAuthorOutbox.length}, rootAuthor=${rootAuthorOutbox.length}, agg=${AGGREGATOR_RELAYS.length})`, + ); const filter: Filter = { kinds: [kind], @@ -1008,10 +1094,19 @@ export class Nip22Adapter extends ChatProtocolAdapter { }; const events = await firstValueFrom( - pool.request(relays, [filter], { eventStore }).pipe(toArray()), + pool.request(allHints, [filter], { eventStore }).pipe(toArray()), ); - return events[0] || null; + const found = events[0] || null; + if (found) { + console.log(`[NIP-22] Found addressable root ${coordinate}`); + } else { + console.warn( + `[NIP-22] Addressable root ${coordinate} not found on any of ${allHints.length} relays`, + ); + } + + return found; } /**