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.
This commit is contained in:
Claude
2026-01-23 08:08:40 +00:00
parent 217ac46e7a
commit ad7aa5a211

View File

@@ -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<NostrEvent | null> {
// 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;
}
/**