Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions agent-memory-mesh/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface MemoryConfig {
* Leave empty for rule-based synthesis (no LLM required).
*/
consolidationModel: string;
/** Directory for memory state snapshots. */
snapshotsDir: string;
}

export function loadConfig(): MemoryConfig {
Expand All @@ -50,5 +52,6 @@ export function loadConfig(): MemoryConfig {
port: Number(env("MEMORY_PORT", "8377")),
apiKey: env("MEMORY_API_KEY", ""),
consolidationModel: env("CONSOLIDATION_MODEL", ""),
snapshotsDir: env("SNAPSHOTS_DIR") || join(base, "snapshots"),
};
}
110 changes: 110 additions & 0 deletions agent-memory-mesh/src/memory/snapshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
readFileSync,
writeFileSync,
existsSync,
mkdirSync,
readdirSync,
copyFileSync,
rmSync,
} from "node:fs";
import { join } from "node:path";
import { randomUUID } from "node:crypto";

export interface SnapshotManifest {
id: string;
createdAt: string;
label?: string;
files: {
workMemoryPath: string;
graphPath: string;
feedbackPath: string;
workMemoryCopy: string;
graphCopy: string;
feedbackCopy: string;
};
}

export class SnapshotStore {
constructor(private snapshotsDir: string) {
mkdirSync(snapshotsDir, { recursive: true });
}

async create(opts: {
workMemoryPath: string;
graphPath: string;
feedbackPath: string;
label?: string;
}): Promise<SnapshotManifest> {
const id = randomUUID();
const dir = join(this.snapshotsDir, id);
mkdirSync(dir, { recursive: true });

const safeCopy = (src: string, dest: string) => {
if (existsSync(src)) copyFileSync(src, dest);
else writeFileSync(dest, "[]");
};

const wCopy = join(dir, "work-memory.json");
const gCopy = join(dir, "graph.json");
const fCopy = join(dir, "feedback.json");
safeCopy(opts.workMemoryPath, wCopy);
safeCopy(opts.graphPath, gCopy);
safeCopy(opts.feedbackPath, fCopy);

const manifest: SnapshotManifest = {
id,
createdAt: new Date().toISOString(),
label: opts.label,
files: {
workMemoryPath: opts.workMemoryPath,
graphPath: opts.graphPath,
feedbackPath: opts.feedbackPath,
workMemoryCopy: wCopy,
graphCopy: gCopy,
feedbackCopy: fCopy,
},
};
writeFileSync(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
return manifest;
}

list(): SnapshotManifest[] {
const subdirs = readdirSync(this.snapshotsDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => join(this.snapshotsDir, d.name, "manifest.json"))
.filter((p) => existsSync(p))
.map((p) => JSON.parse(readFileSync(p, "utf8")) as SnapshotManifest);
return subdirs.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}

get(id: string): SnapshotManifest | undefined {
const manifestPath = join(this.snapshotsDir, id, "manifest.json");
if (!existsSync(manifestPath)) return undefined;
return JSON.parse(readFileSync(manifestPath, "utf8"));
}

async restore(id: string): Promise<void> {
const manifest = this.get(id);
if (!manifest) throw new Error(`Snapshot ${id} not found`);
// Auto-snapshot current state before restoring
await this.create({
workMemoryPath: manifest.files.workMemoryPath,
graphPath: manifest.files.graphPath,
feedbackPath: manifest.files.feedbackPath,
label: `pre-restore-${id.slice(0, 8)}`,
});
if (existsSync(manifest.files.workMemoryCopy))
copyFileSync(manifest.files.workMemoryCopy, manifest.files.workMemoryPath);
if (existsSync(manifest.files.graphCopy))
copyFileSync(manifest.files.graphCopy, manifest.files.graphPath);
if (existsSync(manifest.files.feedbackCopy))
copyFileSync(manifest.files.feedbackCopy, manifest.files.feedbackPath);
}

delete(id: string): boolean {
const dir = join(this.snapshotsDir, id);
if (!existsSync(dir)) return false;
rmSync(dir, { recursive: true, force: true });
return true;
}
}
92 changes: 79 additions & 13 deletions agent-memory-mesh/src/service/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import { Consolidator, type ConsolidationResult } from "../memory/consolidator.j
import { ContextGraph, type ContextGraphEntity, type ContextGraphEdge, type EntityType, type NeighborResult } from "../memory/context-graph.js";
import { RetrievalPolicyStore, applyRecencyBoost, type RetrievalPolicy } from "../memory/retrieval-policy.js";
import { FeedbackStore, applyFeedbackScoring, type FeedbackSignal, type NoteFeedback } from "../memory/feedback.js";
import { SnapshotStore, type SnapshotManifest } from "../memory/snapshots.js";
import { metrics, type MetricsSnapshot } from "./metrics.js";

export { type WorkMemoryEntry, type WorkMemoryQuery, type ConsolidationResult, type ContextGraphEntity, type ContextGraphEdge, type EntityType, type NeighborResult, type RetrievalPolicy, type FeedbackSignal, type NoteFeedback };
export { type WorkMemoryEntry, type WorkMemoryQuery, type ConsolidationResult, type ContextGraphEntity, type ContextGraphEdge, type EntityType, type NeighborResult, type RetrievalPolicy, type FeedbackSignal, type NoteFeedback, type SnapshotManifest, type MetricsSnapshot };

export interface WikiSummary {
entityId: string;
Expand All @@ -41,6 +43,7 @@ export class MemoryEngine {
private graph: ContextGraph;
private policies: RetrievalPolicyStore;
private feedback: FeedbackStore;
private snapshots: SnapshotStore;
private opened = false;

constructor(private cfg: MemoryConfig) {
Expand All @@ -58,6 +61,7 @@ export class MemoryEngine {
});
this.policies = new RetrievalPolicyStore(cfg.policiesPath);
this.feedback = new FeedbackStore(cfg.feedbackPath);
this.snapshots = new SnapshotStore(cfg.snapshotsDir);
}

/** Open the store lazily, sizing the table from a probe embedding the first time. */
Expand All @@ -70,23 +74,40 @@ export class MemoryEngine {

/** Hybrid (vector + keyword) search. Optional metadata filter, e.g. type/tags. */
async search(query: string, k = 8, filter?: string): Promise<RetrievalHit[]> {
await this.ensureOpen();
const qvec = await this.embedder.embed(query);
return this.store.retrieve(query, qvec, k, filter);
const start = Date.now();
try {
await this.ensureOpen();
const qvec = await this.embedder.embed(query);
const hits = await this.store.retrieve(query, qvec, k, filter);
metrics.recordSearch(Date.now() - start);
return hits;
} catch (err) {
metrics.recordSearch(Date.now() - start, true);
throw err;
}
}

/** Rebuild the index from the vault. */
async reindex(onProgress?: (m: string) => void): Promise<IndexResult> {
const indexer = new Indexer(this.cfg.vaultPath, this.store, this.embedder);
const res = await indexer.indexAll(onProgress);
this.opened = true; // indexer opens the store as part of its run
return res;
const start = Date.now();
try {
const indexer = new Indexer(this.cfg.vaultPath, this.store, this.embedder);
const res = await indexer.indexAll(onProgress);
this.opened = true;
metrics.recordIndex(Date.now() - start);
return res;
} catch (err) {
metrics.recordIndex(Date.now() - start, true);
throw err;
}
}

// Work memory — episodic record of agent actions, outputs, and corrections.

recordWork(entry: Omit<WorkMemoryEntry, "id" | "timestamp">): WorkMemoryEntry {
return this.workMemory.record(entry);
const result = this.workMemory.record(entry);
metrics.recordWorkMemory();
return result;
}

queryWork(q: WorkMemoryQuery = {}): WorkMemoryEntry[] {
Expand All @@ -98,17 +119,23 @@ export class MemoryEngine {
}

recordCorrection(sessionId: string, note: string, sourceEntryId?: string): WorkMemoryEntry {
return this.workMemory.recordCorrection(sessionId, note, sourceEntryId);
const result = this.workMemory.recordCorrection(sessionId, note, sourceEntryId);
metrics.recordWorkMemory();
return result;
}

// Consolidation — synthesise sessions into vault lesson notes.

async consolidateAll(): Promise<ConsolidationResult[]> {
return this.consolidator.consolidateAll();
const results = await this.consolidator.consolidateAll();
results.forEach(() => metrics.recordConsolidation());
return results;
}

async consolidateSession(sessionId: string): Promise<ConsolidationResult> {
return this.consolidator.consolidateSession(sessionId);
const result = await this.consolidator.consolidateSession(sessionId);
metrics.recordConsolidation();
return result;
}

// Context graph — entities, edges, and LLM-wiki summaries.
Expand Down Expand Up @@ -226,9 +253,11 @@ export class MemoryEngine {
vote: "up" | "down",
opts: { sessionId?: string; query?: string; note?: string; workMemoryEntryId?: string } = {}
): FeedbackSignal {
return vote === "up"
const signal = vote === "up"
? this.feedback.upvote(notePath, opts)
: this.feedback.downvote(notePath, opts);
metrics.recordFeedback();
return signal;
}

listFeedbackSignals(notePath?: string): FeedbackSignal[] {
Expand Down Expand Up @@ -267,6 +296,43 @@ export class MemoryEngine {
return { processed: processedIds.length, signals };
}

// Metrics

getMetrics(): MetricsSnapshot {
return metrics.snapshot();
}

resetMetrics(): void {
metrics.reset();
}

// Snapshots

async createSnapshot(label?: string): Promise<SnapshotManifest> {
return this.snapshots.create({
workMemoryPath: this.cfg.workMemoryPath,
graphPath: this.cfg.graphPath,
feedbackPath: this.cfg.feedbackPath,
label,
});
}

listSnapshots(): SnapshotManifest[] {
return this.snapshots.list();
}

getSnapshot(id: string): SnapshotManifest | undefined {
return this.snapshots.get(id);
}

async restoreSnapshot(id: string): Promise<void> {
return this.snapshots.restore(id);
}

deleteSnapshot(id: string): boolean {
return this.snapshots.delete(id);
}

private collectWikis(query: string, types?: EntityType[]): WikiSummary[] {
const matches = this.graph.findByName(query);
const candidates = types ? matches.filter((e) => types.includes(e.type)) : matches;
Expand Down
40 changes: 40 additions & 0 deletions agent-memory-mesh/src/service/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,46 @@ export function startHttp(
return send(res, 200, { entity });
}

// Metrics
if (req.method === "GET" && url.pathname === "/metrics") {
return send(res, 200, { metrics: engine.getMetrics() });
}
if (req.method === "POST" && url.pathname === "/metrics/reset") {
engine.resetMetrics();
return send(res, 200, { ok: true });
}

// Snapshots
if (req.method === "POST" && url.pathname === "/snapshots") {
const body = await readJson(req);
const manifest = await engine.createSnapshot(body?.label);
return send(res, 201, { manifest });
}
if (req.method === "GET" && url.pathname === "/snapshots") {
return send(res, 200, { snapshots: engine.listSnapshots() });
}
const snapIdMatch = url.pathname.match(/^\/snapshots\/([^/]+)$/);
if (snapIdMatch) {
const id = decodeURIComponent(snapIdMatch[1]);
if (req.method === "GET") {
const manifest = engine.getSnapshot(id);
if (!manifest) return send(res, 404, { error: "snapshot not found" });
return send(res, 200, { manifest });
}
if (req.method === "DELETE") {
const ok = engine.deleteSnapshot(id);
return send(res, 200, { ok });
}
}
const snapRestoreMatch = url.pathname.match(/^\/snapshots\/([^/]+)\/restore$/);
if (req.method === "POST" && snapRestoreMatch) {
const id = decodeURIComponent(snapRestoreMatch[1]);
const manifest = engine.getSnapshot(id);
if (!manifest) return send(res, 404, { error: "snapshot not found" });
await engine.restoreSnapshot(id);
return send(res, 200, { ok: true });
}

return send(res, 404, { error: "not found" });
} catch (err) {
return send(res, 500, { error: String(err) });
Expand Down
52 changes: 52 additions & 0 deletions agent-memory-mesh/src/service/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,58 @@ export async function startMcpStdio(cfg: MemoryConfig, engine: MemoryEngine): Pr
}
);

server.registerTool(
"create_snapshot",
{
title: "Create memory snapshot",
description:
"Snapshot the current work memory, context graph, and feedback state. " +
"Use before risky operations or experiments to enable rollback.",
inputSchema: {
label: z.string().optional().describe("Human-readable label for this snapshot."),
},
},
async ({ label }) => {
const manifest = await engine.createSnapshot(label);
return { content: [{ type: "text", text: JSON.stringify(manifest, null, 2) }] };
}
);

server.registerTool(
"list_snapshots",
{
title: "List memory snapshots",
description: "List all saved memory snapshots, newest first.",
inputSchema: {},
},
async () => {
const snapshots = engine.listSnapshots();
const text = snapshots.length
? snapshots.map((s) => `[${s.createdAt}] ${s.id}${s.label ? ` — ${s.label}` : ""}`).join("\n")
: "No snapshots found.";
return { content: [{ type: "text", text }] };
}
);

server.registerTool(
"restore_snapshot",
{
title: "Restore memory snapshot",
description:
"Restore work memory, context graph, and feedback to a previously saved snapshot. " +
"A safety backup is automatically created before the restore.",
inputSchema: {
id: z.string().describe("Snapshot ID to restore."),
},
},
async ({ id }) => {
const manifest = engine.getSnapshot(id);
if (!manifest) return { content: [{ type: "text", text: `Snapshot ${id} not found.` }] };
await engine.restoreSnapshot(id);
return { content: [{ type: "text", text: `Restored snapshot ${id}${manifest.label ? ` (${manifest.label})` : ""}.` }] };
}
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[mcp] agent-memory-mesh stdio server ready");
Expand Down
Loading