✨ feat(citations): citation resolution + node-merge redirects#58
Conversation
…lable; harden ids input Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces inline-citation resolution capabilities, allowing the batch-resolution of node, claim, and source IDs into citation-ready records. It implements a new node_redirects table and helper functions to track and follow merged nodes, ensuring stale references can still be resolved. Additionally, it exposes a new POST endpoint /citations/resolve and updates the SDK client. The review feedback focuses on optimizing database queries by deduplicating input IDs in the resolution and redirect helpers, preventing potential PostgreSQL runtime errors by deduplicating IDs during conflict updates, and explicitly importing readBody in the route handler.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| @@ -0,0 +1,16 @@ | |||
| import { defineEventHandler } from "h3"; | |||
There was a problem hiding this comment.
| if (consumedIds.length === 0) return; | ||
|
|
||
| await db | ||
| .update(nodeRedirects) | ||
| .set({ toNodeId: survivorId }) | ||
| .where( | ||
| and( | ||
| eq(nodeRedirects.userId, userId), | ||
| inArray(nodeRedirects.toNodeId, consumedIds), | ||
| ), | ||
| ); | ||
|
|
||
| await db | ||
| .insert(nodeRedirects) | ||
| .values( | ||
| consumedIds.map((fromNodeId) => ({ | ||
| userId, | ||
| fromNodeId, | ||
| toNodeId: survivorId, | ||
| })), | ||
| ) | ||
| .onConflictDoUpdate({ | ||
| target: [nodeRedirects.userId, nodeRedirects.fromNodeId], | ||
| set: { toNodeId: survivorId }, | ||
| }); |
There was a problem hiding this comment.
Deduplicate consumedIds before performing the update and insert operations. If consumedIds contains duplicate IDs, PostgreSQL will throw a runtime error during the INSERT ... ON CONFLICT DO UPDATE statement because a single command cannot affect the same row twice.
if (consumedIds.length === 0) return;
const uniqueConsumedIds = [...new Set(consumedIds)];
await db
.update(nodeRedirects)
.set({ toNodeId: survivorId })
.where(
and(
eq(nodeRedirects.userId, userId),
inArray(nodeRedirects.toNodeId, uniqueConsumedIds),
),
);
await db
.insert(nodeRedirects)
.values(
uniqueConsumedIds.map((fromNodeId) => ({
userId,
fromNodeId,
toNodeId: survivorId,
})),
)
.onConflictDoUpdate({
target: [nodeRedirects.userId, nodeRedirects.fromNodeId],
set: { toNodeId: survivorId },
});| if (ids.length === 0) return out; | ||
|
|
||
| const rows = await db | ||
| .select({ | ||
| fromNodeId: nodeRedirects.fromNodeId, | ||
| toNodeId: nodeRedirects.toNodeId, | ||
| }) | ||
| .from(nodeRedirects) | ||
| .where( | ||
| and( | ||
| eq(nodeRedirects.userId, userId), | ||
| inArray(nodeRedirects.fromNodeId, ids), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Deduplicate ids before querying the database to optimize the query planner and reduce parameter overhead in the IN clause.
| if (ids.length === 0) return out; | |
| const rows = await db | |
| .select({ | |
| fromNodeId: nodeRedirects.fromNodeId, | |
| toNodeId: nodeRedirects.toNodeId, | |
| }) | |
| .from(nodeRedirects) | |
| .where( | |
| and( | |
| eq(nodeRedirects.userId, userId), | |
| inArray(nodeRedirects.fromNodeId, ids), | |
| ), | |
| ); | |
| const uniqueIds = [...new Set(ids)]; | |
| if (uniqueIds.length === 0) return out; | |
| const rows = await db | |
| .select({ | |
| fromNodeId: nodeRedirects.fromNodeId, | |
| toNodeId: nodeRedirects.toNodeId, | |
| }) | |
| .from(nodeRedirects) | |
| .where( | |
| and( | |
| eq(nodeRedirects.userId, userId), | |
| inArray(nodeRedirects.fromNodeId, uniqueIds), | |
| ), | |
| ); |
| const nodeIds = ids.filter((i) => prefixOf(i) === "node") as TypeId<"node">[]; | ||
| const claimIds = ids.filter( | ||
| (i) => prefixOf(i) === "claim", | ||
| ) as TypeId<"claim">[]; | ||
| const sourceIds = ids.filter( | ||
| (i) => prefixOf(i) === "src", | ||
| ) as TypeId<"source">[]; |
There was a problem hiding this comment.
Deduplicate nodeIds, claimIds, and sourceIds before querying the database. This optimizes the query planner and avoids redundant database round-trips/parameters for duplicate requested IDs, while still preserving duplicates in the final output array via the byId map lookup.
const nodeIds = [...new Set(ids.filter((i) => prefixOf(i) === "node"))] as TypeId<"node">[];
const claimIds = [...new Set(ids.filter((i) => prefixOf(i) === "claim"))] as TypeId<"claim">[];
const sourceIds = [...new Set(ids.filter((i) => prefixOf(i) === "src"))] as TypeId<"source">[];
What & why
Adds the memory-side support for Petals' new inline-citation feature:
resolveCitations(ids)— a batch endpoint (POST /citations/resolve) + SDK client method that resolves a mix ofnode_*/claim_*/src_*ids to citation-ready records (title,snippet, provenancesource,available,canonicalId).mergeNodespreviously hard-deleted consumed nodes, so any external reference (e.g. a stored citation) to a merged node id 404'd forever. It now writes anode_redirects(user_id, from_node_id → to_node_id)row per consumed node (re-pointing existing chains so they stay flat) before the delete, andresolveCitationsfollows them.Bumps
@marcelsamyn/memoryto 1.18.0.Changes
node_redirectstable + migration; no FK onfrom_node_id(the consumed node is gone — the redirect must outlive it).writeNodeRedirects/resolveNodeRedirectshelpers;mergeNodeswrites redirects inside its merge transaction.resolveCitationslib +/citations/resolveroute + SDK client method + schema exports.available: false(id durability preserved; staleness flagged).How to test
pnpm test --run src/lib/node-redirects.test.ts src/lib/resolve-citations.test.ts(needs Postgres on:5431).pnpm run build:check && pnpm run build-sdk && pnpm run lint.Checklist
1.18.0after mergeDesign note:
docs/citations-sdk.md. Consumed by the Petals inline-citations PR (Plan 3 / Source Peek).🤖 Generated with Claude Code