From 3a32d4cb3a5229a958f6373b6d8388f6ec3dbde5 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Mon, 29 Jun 2026 22:01:00 +0100 Subject: [PATCH] Backfill task markers by thread reference --- .../messages/lib/auxBackfill.test.mjs | 40 +++++++++++ .../src/features/messages/lib/auxBackfill.ts | 69 +++++++++++++++---- .../shared/api/relayChannelFilters.test.mjs | 8 +++ desktop/src/shared/api/relayChannelFilters.ts | 12 ++++ 4 files changed, 116 insertions(+), 13 deletions(-) diff --git a/desktop/src/features/messages/lib/auxBackfill.test.mjs b/desktop/src/features/messages/lib/auxBackfill.test.mjs index 18cfc5538f..cfa990a306 100644 --- a/desktop/src/features/messages/lib/auxBackfill.test.mjs +++ b/desktop/src/features/messages/lib/auxBackfill.test.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + collectAgentConversationMarkerReferenceIds, collectAuxEventIdsForDeletionBackfill, collectMessageIdsForAuxBackfill, mergeAuxEventsWithDeletionBackfill, @@ -59,6 +60,45 @@ test("returns empty for a window of only auxiliary events", () => { assert.deepEqual(collectMessageIdsForAuxBackfill(events), []); }); +test("collects task marker references from loaded timeline replies", () => { + const rootId = hex("1"); + const parentId = hex("2"); + const replyId = hex("3"); + const events = [ + event(replyId, 9, { + tags: [ + ["h", CHANNEL_ID], + ["e", rootId, "", "root"], + ["e", parentId, "", "reply"], + ], + }), + ]; + + assert.deepEqual(collectAgentConversationMarkerReferenceIds(events), [ + replyId, + rootId, + parentId, + ]); +}); + +test("skips non-content events when collecting task marker references", () => { + const messageId = hex("1"); + const markerId = hex("2"); + const events = [ + event(messageId, 9), + event(markerId, 40004, { + tags: [ + ["h", CHANNEL_ID], + ["e", messageId, "", "root"], + ], + }), + ]; + + assert.deepEqual(collectAgentConversationMarkerReferenceIds(events), [ + messageId, + ]); +}); + test("collects reaction and edit ids for deletion-marker backfill", () => { const events = [ event(hex("1"), 9), diff --git a/desktop/src/features/messages/lib/auxBackfill.ts b/desktop/src/features/messages/lib/auxBackfill.ts index df0272d733..c436b99c22 100644 --- a/desktop/src/features/messages/lib/auxBackfill.ts +++ b/desktop/src/features/messages/lib/auxBackfill.ts @@ -1,11 +1,15 @@ import type { QueryClient } from "@tanstack/react-query"; +import { getThreadReference } from "@/features/messages/lib/threading"; import { channelMessagesKey, sortMessages, } from "@/features/messages/lib/messageQueryKeys"; import { relayClient } from "@/shared/api/relayClient"; -import { buildChannelStructuralAuxFilter } from "@/shared/api/relayChannelFilters"; +import { + buildChannelAgentConversationMarkerFilter, + buildChannelStructuralAuxFilter, +} from "@/shared/api/relayChannelFilters"; import type { RelayEvent } from "@/shared/api/types"; import { CHANNEL_TIMELINE_CONTENT_KINDS, @@ -17,6 +21,10 @@ const TIMELINE_CONTENT_KINDS: ReadonlySet = new Set( CHANNEL_TIMELINE_CONTENT_KINDS, ); +function isTimelineContentEvent(event: RelayEvent) { + return TIMELINE_CONTENT_KINDS.has(event.kind); +} + /** * Extract the ids of the visible content messages from a freshly-fetched * history window. Auxiliary events (reactions, edits, deletions) are then @@ -26,9 +34,30 @@ const TIMELINE_CONTENT_KINDS: ReadonlySet = new Set( export function collectMessageIdsForAuxBackfill( historyEvents: RelayEvent[], ): string[] { - return historyEvents - .filter((event) => TIMELINE_CONTENT_KINDS.has(event.kind)) - .map((event) => event.id); + return historyEvents.filter(isTimelineContentEvent).map((event) => event.id); +} + +export function collectAgentConversationMarkerReferenceIds( + historyEvents: RelayEvent[], +): string[] { + const referenceIds = new Set(); + + for (const event of historyEvents) { + if (!isTimelineContentEvent(event)) { + continue; + } + + referenceIds.add(event.id); + const { parentId, rootId } = getThreadReference(event.tags); + if (rootId) { + referenceIds.add(rootId); + } + if (parentId) { + referenceIds.add(parentId); + } + } + + return [...referenceIds]; } export function collectAuxEventIdsForDeletionBackfill( @@ -86,18 +115,31 @@ export async function backfillAuxForMessages( historyEvents: RelayEvent[], ): Promise { const messageIds = collectMessageIdsForAuxBackfill(historyEvents); - if (messageIds.length === 0) { + const markerReferenceIds = + collectAgentConversationMarkerReferenceIds(historyEvents); + if (messageIds.length === 0 && markerReferenceIds.length === 0) { return; } try { const cacheKey = channelMessagesKey(channelId); const cachedEvents = queryClient.getQueryData(cacheKey) ?? []; - const auxEvents = await relayClient.fetchAuxEventsByReference( - channelId, - messageIds, - buildChannelStructuralAuxFilter, - ); + const [auxEvents, markerEvents] = await Promise.all([ + messageIds.length > 0 + ? relayClient.fetchAuxEventsByReference( + channelId, + messageIds, + buildChannelStructuralAuxFilter, + ) + : Promise.resolve([]), + markerReferenceIds.length > 0 + ? relayClient.fetchAuxEventsByReference( + channelId, + markerReferenceIds, + buildChannelAgentConversationMarkerFilter, + ) + : Promise.resolve([]), + ]); const mergedAuxEvents = await mergeAuxEventsWithDeletionBackfill({ channelId, cachedEvents, @@ -105,16 +147,17 @@ export async function backfillAuxForMessages( fetchAuxEventsForMessages: (...args) => relayClient.fetchAuxDeletionEventsForAuxEvents(...args), }); - if (mergedAuxEvents.length === 0) { + const eventsToMerge = [...markerEvents, ...mergedAuxEvents]; + if (eventsToMerge.length === 0) { return; } queryClient.setQueryData(cacheKey, (current = []) => - sortMessages([...current, ...mergedAuxEvents]), + sortMessages([...current, ...eventsToMerge]), ); } catch (error) { console.error( - "Failed to backfill auxiliary events for channel", + "Failed to backfill timeline reference events for channel", channelId, error, ); diff --git a/desktop/src/shared/api/relayChannelFilters.test.mjs b/desktop/src/shared/api/relayChannelFilters.test.mjs index 3519c82146..943df50d46 100644 --- a/desktop/src/shared/api/relayChannelFilters.test.mjs +++ b/desktop/src/shared/api/relayChannelFilters.test.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + buildChannelAgentConversationMarkerFilter, buildChannelAuxDeletionFilter, buildChannelAuxFilter, buildChannelReactionAuxFilter, @@ -43,3 +44,10 @@ test("buildChannelStructuralAuxFilter excludes reactions", () => { assert.deepEqual(filter["#e"], IDS); assert.equal("#h" in filter, false); }); + +test("buildChannelAgentConversationMarkerFilter scopes task markers by channel and references", () => { + const filter = buildChannelAgentConversationMarkerFilter(CHANNEL, IDS); + assert.deepEqual(filter.kinds, [40004, 40010]); + assert.deepEqual(filter["#h"], [CHANNEL]); + assert.deepEqual(filter["#e"], IDS); +}); diff --git a/desktop/src/shared/api/relayChannelFilters.ts b/desktop/src/shared/api/relayChannelFilters.ts index a00e02945d..6916add0c0 100644 --- a/desktop/src/shared/api/relayChannelFilters.ts +++ b/desktop/src/shared/api/relayChannelFilters.ts @@ -97,6 +97,18 @@ export function buildChannelStructuralAuxFilter( ]); } +export function buildChannelAgentConversationMarkerFilter( + channelId: string, + referencedEventIds: string[], +): RelaySubscriptionFilter { + return { + kinds: [...CHANNEL_TIMELINE_STATE_KINDS], + "#h": [channelId], + "#e": referencedEventIds, + limit: MAX_HISTORICAL_LIMIT, + }; +} + /** * Reactions-only filter for the message rows the GUI is currently rendering. * Keep this separate from structural aux backfill so the slow kind:5 deletion