Files
Krystie dc5dc94d79 Initial commit: Adaptive Recall sidecar for UMP (Phase 5)
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.
2026-07-12 19:24:47 -07:00

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);
});