fix: use backend memoryRelations for edge computation in MCP app

Updated MCP app edge computation to use memoryRelations from backend
instead of fabricating extends edges from spaceId grouping. Falls back
to parentMemoryId for backwards compatibility.
This commit is contained in:
Vorflux AI 2026-03-28 23:15:28 +00:00
parent 46d851a2f8
commit 4de8b8f987
6 changed files with 111 additions and 79 deletions

View file

@ -37,6 +37,8 @@ interface GraphApiMemory {
rootMemoryId: string | null
createdAt: string
updatedAt: string
relation?: "updates" | "extends" | "derives" | null
memoryRelations?: Record<string, "updates" | "extends" | "derives"> | null
}
interface GraphApiDocument {
@ -174,9 +176,6 @@ function transformData(data: ToolResultData): {
const nodeIds = new Set<string>()
const SPREAD = 50
// Group documents by spaceId for extends edges
const spaceGroups = new Map<string, string[]>()
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<string, string> = {}
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<string>()
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 })
}
}
}

View file

@ -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<string, "updates" | "extends" | "derives"> | 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,
}
}

View file

@ -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<string, MemoryRelation> | null
// Source/join fields
sourceAddedAt?: string | null
sourceRelevanceScore?: number | null
sourceMetadata?: Record<string, unknown> | null
spaceContainerTag?: string | null
}
export interface DocumentWithMemories {

View file

@ -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<targetId, relationType>) 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<string, string> = {}
// Extends edges: connect documents that share a spaceId
const spaceGroups = new Map<string, string[]>()
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<string>()
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,
})
}
}
}

View file

@ -323,6 +323,8 @@ export function generateMockGraphData(options: MockGraphOptions = {}): {
let version = 1
let isLatest = true
let isForgotten = false
let memoryRelations: Record<string, "updates" | "extends" | "derives"> =
{}
// 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,
})
}

View file

@ -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<string, MemoryRelation> | 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<string, MemoryRelation> | null
}
export interface GraphNode {