dc5dc94d79
Multi-channel retrieval sidecar over Universal Memory Protocol: - 3-channel RRF (UMP FTS5 + Qdrant vector + knowledge graph) - ACT-R re-ranking (Anderson 1983) with access tracking - Co-occurrence graph edges (Phase 6) for dense traversal - Memory lifecycle decay (Phase 4) with per-kind confidence - MCP shim routes recall through sidecar, falls back to canonical UMP Architecture: - src/server.js HTTP sidecar on port 4380 - src/graph.js 2592-node / 111-edge graph from UMP (or +cooccur: 13k+) - src/actr.js A_i = -d*ln(age) + beta*log1p(freq) + epsilon*conf - src/access_log.js per-URN counter + last_accessed_at - src/ump-recall-mcp.js MCP shim (recall via sidecar, others passthrough) Eval results (851-record UMP corpus): - 2ch RRF over baseline: +50pp recall@10 - 3ch RRF (+graph): +60pp, 12 unique wins - ACT-R re-rank: 4/20 #1 changes, 84% top-5 retention Tests: 76/76 passing across graph (27), actr (27), access_log (28), decay (20), mcp-shim (sidecar + fallback). Run with: npm test Inspired by AIAppsAPI/adaptive-recall but built from scratch against existing DNS2 infrastructure (UMP at :4317, Qdrant at :6333, Ollama at :11434). No paid SaaS, MIT-licensed.
83 lines
2.4 KiB
JavaScript
83 lines
2.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Phase 2A: Build the knowledge graph from the live UMP store and persist
|
|
// it to GRAPH_FILE (default /root/ump-recall/state/graph.json).
|
|
//
|
|
// Usage:
|
|
// node scripts/build_graph.js # default UMP file
|
|
// UMP_FILE=/path/to/x.json node scripts/build_graph.js
|
|
// GRAPH_FILE=/tmp/x.json node scripts/build_graph.js
|
|
//
|
|
// Output: prints stats, top-10 entities by frequency, and top-10 edges by
|
|
// weight. Exits 0 on success. Reads the UMP file as JSON once; suitable
|
|
// for cron / shell triggers.
|
|
|
|
import { promises as fs } from "node:fs";
|
|
import { Graph } from "../src/graph.js";
|
|
|
|
const UMP_FILE =
|
|
process.env.UMP_FILE ||
|
|
"/root/.openclaw/agents/main/workspace/state/ump-local/memory.ump.json";
|
|
|
|
async function loadRecords() {
|
|
const t0 = Date.now();
|
|
const raw = await fs.readFile(UMP_FILE, "utf8");
|
|
const records = JSON.parse(raw);
|
|
if (!Array.isArray(records)) {
|
|
throw new Error(`UMP file root must be an array, got ${typeof records}`);
|
|
}
|
|
const dt = Date.now() - t0;
|
|
return { records, parseMs: dt };
|
|
}
|
|
|
|
async function main() {
|
|
const startedAt = Date.now();
|
|
const { records, parseMs } = await loadRecords();
|
|
console.log(
|
|
`loaded ${records.length} records from ${UMP_FILE} in ${parseMs} ms`
|
|
);
|
|
|
|
const g = new Graph();
|
|
const buildT0 = Date.now();
|
|
g.buildFromRecords(records);
|
|
const buildMs = Date.now() - buildT0;
|
|
|
|
const saveRes = await g.save();
|
|
console.log(
|
|
`built graph in ${buildMs} ms; saved -> ${saveRes.file} ` +
|
|
`(nodes=${saveRes.nodes}, edges=${saveRes.edges}, urns=${saveRes.urns})`
|
|
);
|
|
|
|
const total = Date.now() - startedAt;
|
|
console.log(`\ntotal: ${total} ms\n`);
|
|
|
|
// Reporting.
|
|
console.log("---- Top 10 entities by frequency ----");
|
|
for (const e of g.topEntities(10)) {
|
|
console.log(
|
|
` ${String(e.frequency).padStart(4)}x ${e.kind.padEnd(14)} ${e.entity}` +
|
|
` (in ${e.urns} urns)`
|
|
);
|
|
}
|
|
|
|
console.log("\n---- Top 10 edges by weight ----");
|
|
for (const e of g.topEdges(10)) {
|
|
console.log(
|
|
` ${String(e.weight).padStart(3)}x ${e.src} ${arrow(e.type)} ${e.tgt}` +
|
|
` (${e.type}, in ${e.urns} urns)`
|
|
);
|
|
}
|
|
|
|
console.log("\n---- Stats ----");
|
|
console.log(JSON.stringify(g.stats(), null, 2));
|
|
}
|
|
|
|
function arrow(type) {
|
|
if (type === "relates-to") return "─▶"; // matches the relation convention
|
|
return "──";
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error("FATAL:", e?.stack || e?.message || e);
|
|
process.exit(1);
|
|
});
|