Add temporal versioning for node history tracking (Milestone 1)
Enable time-travel queries and history viewing by creating immutable version records on every node update. Includes database schema changes, store functions, MCP tools, and CLI commands for viewing history, comparing versions, and restoring to previous states.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod/v3';
|
||||
import { query, listNodes, getNode, findNodeByPrefix, addNode, addEdge, removeNode, removeEdge, updateNode } from '../core/store';
|
||||
import { query, listNodes, getNode, findNodeByPrefix, addNode, addEdge, removeNode, removeEdge, updateNode, getNodeHistory, getNodeAtTime, getNodeVersion, diffVersions, restoreVersion } from '../core/store';
|
||||
import { getConnections, getEdgesByNode } from '../core/graph';
|
||||
import { cosineSimilarity } from '../core/search/vector';
|
||||
import { getDb } from '../core/db';
|
||||
@@ -400,6 +400,102 @@ server.tool(
|
||||
}
|
||||
);
|
||||
|
||||
// --- memory_history ---
|
||||
server.tool(
|
||||
'memory_history',
|
||||
'Get version history for a node',
|
||||
{
|
||||
id: z.string().describe('Node ID or prefix'),
|
||||
},
|
||||
async ({ id }) => {
|
||||
const node = getNode(id) ?? findNodeByPrefix(id);
|
||||
if (!node) {
|
||||
return { content: [{ type: 'text' as const, text: serialize({ error: 'Node not found' }) }], isError: true };
|
||||
}
|
||||
const history = getNodeHistory(node.id);
|
||||
return { content: [{ type: 'text' as const, text: serialize({ nodeId: node.id, title: node.title, versions: history }) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// --- memory_show_at ---
|
||||
server.tool(
|
||||
'memory_show_at',
|
||||
'Show node at a specific point in time',
|
||||
{
|
||||
id: z.string().describe('Node ID or prefix'),
|
||||
timestamp: z.union([z.number(), z.string()]).describe('Unix ms or ISO date string'),
|
||||
},
|
||||
async ({ id, timestamp }) => {
|
||||
const node = getNode(id) ?? findNodeByPrefix(id);
|
||||
if (!node) {
|
||||
return { content: [{ type: 'text' as const, text: serialize({ error: 'Node not found' }) }], isError: true };
|
||||
}
|
||||
|
||||
// Parse timestamp
|
||||
let ts: number;
|
||||
if (typeof timestamp === 'number') {
|
||||
ts = timestamp;
|
||||
} else {
|
||||
const parsed = Date.parse(timestamp);
|
||||
if (isNaN(parsed)) {
|
||||
return { content: [{ type: 'text' as const, text: serialize({ error: 'Invalid timestamp format' }) }], isError: true };
|
||||
}
|
||||
ts = parsed;
|
||||
}
|
||||
|
||||
const historical = getNodeAtTime(node.id, ts);
|
||||
if (!historical) {
|
||||
return { content: [{ type: 'text' as const, text: serialize({ error: 'No version found for the specified time' }) }], isError: true };
|
||||
}
|
||||
return { content: [{ type: 'text' as const, text: serialize(historical) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// --- memory_diff ---
|
||||
server.tool(
|
||||
'memory_diff',
|
||||
'Compare two versions of a node',
|
||||
{
|
||||
id: z.string().describe('Node ID or prefix'),
|
||||
v1: z.number().describe('First version number'),
|
||||
v2: z.number().describe('Second version number'),
|
||||
},
|
||||
async ({ id, v1, v2 }) => {
|
||||
const node = getNode(id) ?? findNodeByPrefix(id);
|
||||
if (!node) {
|
||||
return { content: [{ type: 'text' as const, text: serialize({ error: 'Node not found' }) }], isError: true };
|
||||
}
|
||||
|
||||
const diff = diffVersions(node.id, v1, v2);
|
||||
if (!diff) {
|
||||
return { content: [{ type: 'text' as const, text: serialize({ error: 'One or both versions not found' }) }], isError: true };
|
||||
}
|
||||
return { content: [{ type: 'text' as const, text: serialize(diff) }] };
|
||||
}
|
||||
);
|
||||
|
||||
// --- memory_restore ---
|
||||
server.tool(
|
||||
'memory_restore',
|
||||
'Restore a node to a previous version (creates new version)',
|
||||
{
|
||||
id: z.string().describe('Node ID or prefix'),
|
||||
version: z.number().describe('Version number to restore'),
|
||||
},
|
||||
async ({ id, version }) => {
|
||||
const node = getNode(id) ?? findNodeByPrefix(id);
|
||||
if (!node) {
|
||||
return { content: [{ type: 'text' as const, text: serialize({ error: 'Node not found' }) }], isError: true };
|
||||
}
|
||||
|
||||
const restored = await restoreVersion(node.id, version);
|
||||
if (!restored) {
|
||||
return { content: [{ type: 'text' as const, text: serialize({ error: 'Version not found' }) }], isError: true };
|
||||
}
|
||||
return { content: [{ type: 'text' as const, text: serialize({ message: `Restored to version ${version}`, node: restored }) }] };
|
||||
}
|
||||
);
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
Reference in New Issue
Block a user