Backfill task markers by thread reference

This commit is contained in:
klopez4212
2026-06-29 22:01:00 +01:00
parent 35e2c32865
commit 3a32d4cb3a
4 changed files with 116 additions and 13 deletions

View File

@@ -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),

View File

@@ -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<number> = 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<number> = 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<string>();
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<void> {
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<RelayEvent[]>(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<RelayEvent[]>(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,
);

View File

@@ -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);
});

View File

@@ -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