feat: RAG memory graph with interlinked traversal#13
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a new “memory graph” subsystem to support RAG-style persistence and Obsidian-like interlinked traversal, exposing it via new MCP tools and updating docs/landing copy to reflect the new capabilities.
Changes:
- Introduces a JSON-persisted memory graph core with node upsert, typed relations, similarity auto-linking, traversal, and pruning.
- Adds MCP tool wrappers + registers 6 new MCP tools on the server.
- Adds a comprehensive test suite and updates landing/docs messaging for RAG + memory graph features.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
src/core/memory-graph.ts |
Implements the persistent property graph, traversal, decay scoring, pruning, and interlinking. |
src/tools/memory-tools.ts |
Adds MCP-friendly wrapper functions that format output for the new graph operations. |
src/index.ts |
Registers the 6 new MCP tools (upsert_memory_node, create_relation, etc.) with Zod schemas. |
test/main/memory-graph.test.mjs |
Adds tests for core graph behavior and MCP wrapper formatting. |
landing/src/components/ToolDiagram.tsx |
Adds a “RAG Functions” panel/label to the landing tool diagram. |
landing/src/components/IsometricDiagram.tsx |
Adds new memory/RAG capability cards and tweaks layout spacing. |
landing/src/components/InstructionsSection.tsx |
Documents the new memory tools + updates tool count and guidance text. |
landing/src/components/IdeSetup.tsx |
Updates displayed env config snippet (removes some env vars). |
landing/src/app/page.tsx |
Updates hero copy to mention RAG. |
landing/src/app/layout.tsx |
Updates metadata description to mention RAG. |
landing/src/app/api/instructions/route.ts |
Updates the served instructions text to include memory graph + tools. |
README.md |
Adds “Memory & RAG” tool section and updates architecture/tool counts. |
INSTRUCTIONS.md |
Updates instruction content to include memory graph + tools and tool counts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| > | ||
| Context+ is an MCP server designed for developers who demand 99% | ||
| accuracy. By combining Tree-sitter AST parsing & Spectral | ||
| accuracy. By combining RAG, Treesitter AST & Spectral |
There was a problem hiding this comment.
Spelling/branding inconsistency: this sentence uses “Treesitter”, but other docs (e.g. README) use “Tree-sitter”. Consider using “Tree-sitter” consistently.
| accuracy. By combining RAG, Treesitter AST & Spectral | |
| accuracy. By combining RAG, Tree-sitter AST & Spectral |
| y1="0" | ||
| x2="0" | ||
| y2="6" | ||
| stroke={isDark ? "#000000" : "#000000"} |
There was a problem hiding this comment.
This ternary is redundant: stroke is set to #000000 for both dark and light modes, which likely wasn’t intended and makes the conditional misleading. Consider either removing the conditional or using different stroke colors for each theme.
| stroke={isDark ? "#000000" : "#000000"} | |
| stroke="#000000" |
| function scheduleSave(rootDir: string): void { | ||
| const existing = saveTimeout.get(rootDir); | ||
| if (existing) clearTimeout(existing); | ||
| savePending.set(rootDir, true); | ||
| saveTimeout.set(rootDir, setTimeout(() => { | ||
| if (savePending.get(rootDir)) { | ||
| persistGraph(rootDir).catch(() => {}).finally(() => savePending.set(rootDir, false)); | ||
| } | ||
| }, 500)); | ||
| } |
There was a problem hiding this comment.
scheduleSave can drop updates: if a save is in-flight and scheduleSave(rootDir) is called again, the earlier persistGraph(...).finally(() => savePending.set(rootDir, false)) may flip savePending back to false and prevent the newly-scheduled timer from persisting the latest changes. Consider tracking an in-flight save promise / revision counter per rootDir, or only clearing the pending flag if no newer writes were scheduled after the save started.
| export async function createRelation(rootDir: string, sourceId: string, targetId: string, relation: RelationType, weight?: number, metadata?: Record<string, string>): Promise<MemoryEdge | null> { | ||
| const graph = await loadGraph(rootDir); | ||
| if (!graph.nodes[sourceId] || !graph.nodes[targetId]) return null; | ||
|
|
||
| const duplicate = Object.values(graph.edges).find(e => | ||
| e.source === sourceId && e.target === targetId && e.relation === relation | ||
| ); | ||
| if (duplicate) { | ||
| duplicate.weight = weight ?? duplicate.weight; | ||
| if (metadata) Object.assign(duplicate.metadata, metadata); | ||
| scheduleSave(rootDir); | ||
| return duplicate; | ||
| } |
There was a problem hiding this comment.
Edge directionality is inconsistent: traversal (getEdgesForNode + getNeighborId) treats edges as undirected, but createRelation de-dupes only when source === sourceId && target === targetId. Calling createRelation(A,B,rel) and later createRelation(B,A,rel) will create two edges that traversal treats as equivalent. Either (a) enforce directionality in traversal, or (b) canonicalize/de-dupe relations regardless of direction for symmetric relations (especially similar_to).
| source_id: z.string().describe("ID of the source memory node."), | ||
| target_id: z.string().describe("ID of the target memory node."), | ||
| relation: z.enum(["relates_to", "depends_on", "implements", "references", "similar_to", "contains"]).describe("Relationship type between nodes."), | ||
| weight: z.number().optional().describe("Edge weight 0-1. Higher = stronger relationship. Default: 1.0."), |
There was a problem hiding this comment.
The weight input is documented as 0–1, but the Zod schema accepts any number. Passing negatives or very large values will skew decay/scoring and pruning behavior. Consider adding validation (e.g., .min(0).max(1)) or normalizing weights consistently with the other tools’ patterns.
| weight: z.number().optional().describe("Edge weight 0-1. Higher = stronger relationship. Default: 1.0."), | |
| weight: z.number().min(0).max(1).optional().describe("Edge weight 0-1. Higher = stronger relationship. Default: 1.0."), |
| await mkdir(join(FIXTURE, ".mcp_data"), { recursive: true }); | ||
| }); | ||
|
|
||
| after(async () => { |
There was a problem hiding this comment.
The fixture directory is removed in after(), but memory-graph persists via a 500ms setTimeout. A pending save triggered by the last test can run after this rm() and recreate test/_memory_graph_fixtures (because ensureMcpDataDir uses recursive mkdir). Consider waiting for pending saves to flush before cleanup (or exposing a test-only flush/close helper from the module).
| after(async () => { | |
| after(async () => { | |
| // Wait for any pending debounced saves (memory-graph uses a 500ms timeout) | |
| await new Promise((resolve) => setTimeout(resolve, 600)); |
| import { describe, it, before, after, beforeEach } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { mkdir, rm, readFile } from "fs/promises"; | ||
| import { join, resolve } from "path"; | ||
| import { Ollama } from "ollama"; | ||
|
|
||
| const { | ||
| upsertNode, | ||
| createRelation, | ||
| searchGraph, | ||
| pruneStaleLinks, | ||
| addInterlinkedContext, | ||
| retrieveWithTraversal, | ||
| getGraphStats, | ||
| } = await import("../../build/core/memory-graph.js"); | ||
|
|
||
| const { | ||
| toolUpsertMemoryNode, | ||
| toolCreateRelation, | ||
| toolSearchMemoryGraph, | ||
| toolPruneStaleLinks, | ||
| toolAddInterlinkedContext, | ||
| toolRetrieveWithTraversal, | ||
| } = await import("../../build/tools/memory-tools.js"); | ||
|
|
||
| const FIXTURE = resolve("test/_memory_graph_fixtures"); | ||
| let embedCounter = 0; | ||
|
|
There was a problem hiding this comment.
There are several unused imports/variables here (beforeEach, readFile, and embedCounter is incremented but never asserted). If this repo runs linting on tests, this will fail CI; otherwise it still adds noise. Consider removing unused imports/vars or asserting the counter to validate batching behavior.
| title: "Context+ // Semantic Intelligence for Large-Scale Engineering", | ||
| description: | ||
| "MCP server designed for developers who demand 99% accuracy. Tree-sitter AST parsing, Spectral Clustering, and Obsidian-style linking.", | ||
| "MCP server designed for developers who demand 99% accuracy. RAG, Treesitter AST, Spectral Clustering, and Obsidian-style linking.", |
There was a problem hiding this comment.
Spelling/branding inconsistency: the project uses “Tree-sitter” elsewhere, but this copy uses “Treesitter”. Consider standardizing to “Tree-sitter” for consistency and searchability.
| "MCP server designed for developers who demand 99% accuracy. RAG, Treesitter AST, Spectral Clustering, and Obsidian-style linking.", | |
| "MCP server designed for developers who demand 99% accuracy. RAG, Tree-sitter AST, Spectral Clustering, and Obsidian-style linking.", |
Adds RAG memory graph engine with Obsidian-style interlinked traversal, 6 new MCP tools (17 total), 24 tests, and landing page updates reflecting RAG capabilities.