diff --git a/apps/mcp/src/ui/mcp-app.ts b/apps/mcp/src/ui/mcp-app.ts index 504f190d..ba20d89a 100644 --- a/apps/mcp/src/ui/mcp-app.ts +++ b/apps/mcp/src/ui/mcp-app.ts @@ -37,6 +37,8 @@ interface GraphApiMemory { rootMemoryId: string | null createdAt: string updatedAt: string + relation?: "updates" | "extends" | "derives" | null + memoryRelations?: Record | null } interface GraphApiDocument { @@ -174,9 +176,6 @@ function transformData(data: ToolResultData): { const nodeIds = new Set() const SPREAD = 50 - // Group documents by spaceId for extends edges - const spaceGroups = new Map() - for (const doc of data.documents) { const pos = initialPosition(doc.id, SPREAD) nodes.push({ @@ -214,42 +213,31 @@ function transformData(data: ToolResultData): { } as MemoryNode) nodeIds.add(mem.id) - // Derives link + // Derives link (doc -> memory) links.push({ source: doc.id, target: mem.id, edgeType: "derives" }) - // Updates link - if (mem.parentMemoryId && nodeIds.has(mem.parentMemoryId)) { - links.push({ - source: mem.parentMemoryId, - target: mem.id, - edgeType: "updates", - }) + // Memory-to-memory relation edges from backend data. + // Uses memoryRelations as primary source, falls back to parentMemoryId. + let relations: Record = {} + if ( + mem.memoryRelations && + typeof mem.memoryRelations === "object" && + Object.keys(mem.memoryRelations).length > 0 + ) { + relations = mem.memoryRelations + } else if (mem.parentMemoryId) { + relations = { [mem.parentMemoryId]: "updates" } } - // Track space groups for extends edges - if (mem.spaceId) { - const group = spaceGroups.get(mem.spaceId) - if (group) group.push(doc.id) - else spaceGroups.set(mem.spaceId, [doc.id]) - } - } - } - - // Extends edges between documents sharing a space - const addedEdges = new Set() - for (const docIds of spaceGroups.values()) { - const unique = [...new Set(docIds)] - for (let i = 0; i < unique.length; i++) { - for (let j = i + 1; j < unique.length; j++) { - const key = `${unique[i]}:${unique[j]}` - if (!addedEdges.has(key)) { - addedEdges.add(key) - links.push({ - source: unique[i]!, - target: unique[j]!, - edgeType: "extends", - }) - } + for (const [targetId, relationType] of Object.entries(relations)) { + if (!nodeIds.has(targetId)) continue + const edgeType = + relationType === "updates" || + relationType === "extends" || + relationType === "derives" + ? relationType + : "updates" + links.push({ source: targetId, target: mem.id, edgeType }) } } } diff --git a/apps/web/components/memory-graph/hooks/use-graph-api.ts b/apps/web/components/memory-graph/hooks/use-graph-api.ts index 118ed465..3984088d 100644 --- a/apps/web/components/memory-graph/hooks/use-graph-api.ts +++ b/apps/web/components/memory-graph/hooks/use-graph-api.ts @@ -18,6 +18,7 @@ interface UseGraphApiOptions { interface ApiMemoryEntry { id: string memory: string + content?: string | null spaceId: string isStatic?: boolean isLatest?: boolean @@ -29,6 +30,11 @@ interface ApiMemoryEntry { rootMemoryId?: string | null createdAt: string updatedAt: string + relation?: "updates" | "extends" | "derives" | null + updatesMemoryId?: string | null + nextVersionId?: string | null + memoryRelations?: Record | null + spaceContainerTag?: string | null } interface ApiDocument { @@ -54,7 +60,7 @@ interface ApiDocumentsResponse { function toGraphMemory(mem: ApiMemoryEntry): GraphApiMemory { return { id: mem.id, - memory: mem.memory, + memory: mem.memory ?? mem.content ?? "", isStatic: mem.isStatic ?? false, spaceId: mem.spaceId ?? "", isLatest: mem.isLatest ?? true, @@ -66,6 +72,11 @@ function toGraphMemory(mem: ApiMemoryEntry): GraphApiMemory { rootMemoryId: mem.rootMemoryId ?? null, createdAt: mem.createdAt, updatedAt: mem.updatedAt, + relation: mem.relation ?? null, + updatesMemoryId: mem.updatesMemoryId ?? null, + nextVersionId: mem.nextVersionId ?? null, + memoryRelations: mem.memoryRelations ?? null, + spaceContainerTag: mem.spaceContainerTag ?? null, } } diff --git a/packages/memory-graph/src/api-types.ts b/packages/memory-graph/src/api-types.ts index 48160d8f..631ae715 100644 --- a/packages/memory-graph/src/api-types.ts +++ b/packages/memory-graph/src/api-types.ts @@ -1,9 +1,12 @@ +export type MemoryRelation = "updates" | "extends" | "derives" + export interface MemoryEntry { id: string - content: string + content: string | null + memory?: string | null createdAt: string updatedAt: string - spaceId?: string + spaceId?: string | null embedding?: number[] isStatic?: boolean isForgotten?: boolean @@ -13,6 +16,16 @@ export interface MemoryEntry { parentMemoryId?: string | null rootMemoryId?: string | null isLatest?: boolean + // Relation fields from backend + relation?: MemoryRelation | null + updatesMemoryId?: string | null + nextVersionId?: string | null + memoryRelations?: Record | null + // Source/join fields + sourceAddedAt?: string | null + sourceRelevanceScore?: number | null + sourceMetadata?: Record | null + spaceContainerTag?: string | null } export interface DocumentWithMemories { diff --git a/packages/memory-graph/src/hooks/use-graph-data.ts b/packages/memory-graph/src/hooks/use-graph-data.ts index 68d6060d..d539e86c 100644 --- a/packages/memory-graph/src/hooks/use-graph-data.ts +++ b/packages/memory-graph/src/hooks/use-graph-data.ts @@ -180,7 +180,7 @@ export function useGraphData( for (const mem of doc.memories) allNodeIds.add(mem.id) } - // Derives edges (doc -> memory) + // 1. Derives edges: document -> memory (structural) for (const doc of documents) { for (const mem of doc.memories) { result.push({ @@ -193,50 +193,39 @@ export function useGraphData( } } - // Updates edges (version chain) + // 2. Memory-to-memory relation edges from backend data. + // Uses memoryRelations (Record) as primary source, + // falls back to parentMemoryId for legacy data. for (const doc of documents) { for (const mem of doc.memories) { - if (mem.parentMemoryId && allNodeIds.has(mem.parentMemoryId)) { - result.push({ - id: `ver-${mem.parentMemoryId}-${mem.id}`, - source: mem.parentMemoryId, - target: mem.id, - visualProps: getEdgeVisualProps("updates"), - edgeType: "updates", - }) - } - } - } + let relations: Record = {} - // Extends edges: connect documents that share a spaceId - const spaceGroups = new Map() - for (const doc of documents) { - for (const mem of doc.memories) { - const group = spaceGroups.get(mem.spaceId) - if (group) { - if (!group.includes(doc.id)) group.push(doc.id) - } else { - spaceGroups.set(mem.spaceId, [doc.id]) + if ( + mem.memoryRelations && + typeof mem.memoryRelations === "object" && + Object.keys(mem.memoryRelations).length > 0 + ) { + relations = mem.memoryRelations + } else if (mem.parentMemoryId) { + // Legacy fallback: parentMemoryId implies "updates" + relations = { [mem.parentMemoryId]: "updates" } } - } - } - const addedPairs = new Set() - for (const docIds of spaceGroups.values()) { - for (let i = 0; i < docIds.length; i++) { - for (let j = i + 1; j < docIds.length; j++) { - const a = docIds[i]! - const b = docIds[j]! - const key = a < b ? `${a}:${b}` : `${b}:${a}` - if (!addedPairs.has(key)) { - addedPairs.add(key) - result.push({ - id: `ss-${key}`, - source: a, - target: b, - visualProps: getEdgeVisualProps("extends"), - edgeType: "extends", - }) - } + + for (const [targetId, relationType] of Object.entries(relations)) { + if (!allNodeIds.has(targetId)) continue + const edgeType = + relationType === "updates" || + relationType === "extends" || + relationType === "derives" + ? relationType + : "updates" + result.push({ + id: `rel-${targetId}-${mem.id}`, + source: targetId, + target: mem.id, + visualProps: getEdgeVisualProps(edgeType), + edgeType, + }) } } } diff --git a/packages/memory-graph/src/mock-data.ts b/packages/memory-graph/src/mock-data.ts index 78ad5ff5..e32adb13 100644 --- a/packages/memory-graph/src/mock-data.ts +++ b/packages/memory-graph/src/mock-data.ts @@ -323,6 +323,8 @@ export function generateMockGraphData(options: MockGraphOptions = {}): { let version = 1 let isLatest = true let isForgotten = false + let memoryRelations: Record = + {} // Build version chain for first few memories if applicable if (hasVersionChain && m < 3) { @@ -342,12 +344,27 @@ export function generateMockGraphData(options: MockGraphOptions = {}): { chainPrevId = memId isLatest = m === 2 // last in the 3-memory chain isForgotten = !isLatest && random() < 0.2 + // Add "updates" relation to parent + if (parentMemoryId) { + memoryRelations = { [parentMemoryId]: "updates" } + } } } else { // Standalone memory isForgotten = random() < 0.1 isLatest = true version = 1 + // Randomly add extends/derives relations to earlier memories in this doc + if (m > 0 && random() < 0.2) { + const targetIdx = Math.floor(random() * m) + const targetMem = memories[targetIdx] + if (targetMem) { + const relType = random() < 0.5 ? "extends" : "derives" + memoryRelations = { + [targetMem.id]: relType as "extends" | "derives", + } + } + } } // Determine forgetAfter (for some non-forgotten memories, set a future expiry) @@ -376,6 +393,8 @@ export function generateMockGraphData(options: MockGraphOptions = {}): { rootMemoryId, createdAt: memCreatedAt, updatedAt: memUpdatedAt, + memoryRelations: + Object.keys(memoryRelations).length > 0 ? memoryRelations : undefined, }) } diff --git a/packages/memory-graph/src/types.ts b/packages/memory-graph/src/types.ts index b8b99b7d..40927a1b 100644 --- a/packages/memory-graph/src/types.ts +++ b/packages/memory-graph/src/types.ts @@ -1,8 +1,11 @@ // Graph API types matching backend response +export type MemoryRelation = "updates" | "extends" | "derives" + export interface GraphApiMemory { id: string memory: string + content?: string | null isStatic: boolean spaceId: string isLatest: boolean @@ -14,6 +17,13 @@ export interface GraphApiMemory { rootMemoryId: string | null createdAt: string updatedAt: string + // Relation fields from backend + relation?: MemoryRelation | null + updatesMemoryId?: string | null + nextVersionId?: string | null + memoryRelations?: Record | null + // Source/join fields + spaceContainerTag?: string | null } export interface GraphApiDocument { @@ -59,6 +69,8 @@ export interface MemoryNodeData { spaceId: string createdAt: string updatedAt: string + relation?: MemoryRelation | null + memoryRelations?: Record | null } export interface GraphNode {