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.
5.8 KiB
5.8 KiB
Adaptive Recall Phase 2 + Phase 4 — Final Report
Date: 2026-07-12
Build context: Adaptive Recall for both Hermes and Krystie, sidecar on port 4380, 851 UMP records, 768/851 in Qdrant memories_ump.
Phase 2 (Knowledge Graph Channel)
What was built
| File | Lines | Purpose |
|---|---|---|
src/graph.js |
414 | Graph class with load/save/buildFromRecords/addEntity/addEdge/neighbors/searchEntities/topEntities/topEdges/stats |
scripts/build_graph.js |
82 | Standalone script that loads UMP records and builds/persists the graph |
state/graph.json |
— | Built artifact: 2592 nodes, 111 edges, 719 URNs |
state/SCHEMA.md |
— | Documentation of actual UMP record schema (corrects compaction summary) |
test/test_graph.js |
— | 10+ unit tests (the graph test file from subagent) |
Wiring
src/server.js modified to:
- Import
Graphfrom./graph.js - Add lazy-loaded singleton
getGraph()that loads fromGRAPH_FILE(env-overridable) - Add
runGraphChannel(): extracts entities from query, BFS up toGRAPH_DEPTHhops (default 2), score = 1/hops - Wire 3rd channel into
/recall:Promise.allSettled([runUmpChannel, runVectorChannel, runGraphChannel]) - Update
/healthto report graph stats ({enabled, loaded, nodes, edges, urns, file}) and bump phase label to2C - Default weights now
{ump: 1.0, vector: 1.0, graph: 1.0}
Eval results
Channel-contribution eval (scripts/eval_channel_contribution.py):
Avg graph-channel hits in top-5:
3ch (graph enabled): 0.85 hits/query
2ch (graph zero-weigh): 0.30 hits/query
Delta: +0.55
Hits in 3ch top-5 that are NOT in 2ch top-5:
Total unique: 12 (avg 0.60/query)
Latency: 2ch=903ms 3ch=1079ms Δ=+176ms (graph overhead)
Notable graph rank-#1 wins (queries where graph found a hit that ranked higher than ump+vector):
- "Triangles fuzz harness CI gflags missing" → graph rank #1
- "Krystie OpenClaw migrated Hermes profile" → graph rank #1
- "Triangles test coverage PR keystore V5 soft cap" → graph rank #1
- "Triangles multisig stack walk test audit" → graph rank #3
- "DashCaddy TOTP recovery panel disable bak fallback" → graph rank #3
- "Triangles PoW PoS cutoff nonce zero" → graph rank #4
Zero regressions — 10/20 queries saw graph-channel hits in top-5; the other 10 stayed at the same rank.
Phase 4 (Memory Lifecycle Decay)
What was built
| File | Purpose |
|---|---|
scripts/ump_decay.py |
Standalone Python script with pure apply_decay(records, dry_run) function, atomic writes, timestamped backups |
test/test_ump_decay.py |
20 unit tests across 10 test cases, stdlib-only |
Cron eaff2d9683fc |
Runs nightly at 3am |
Decay rates (per day since last access)
| Kind | λ | Notes |
|---|---|---|
| identity | 0.0001 | very slow, almost never decays |
| semantic | 0.001 | facts over months |
| procedural | 0.005 | medium, skills fade if unused |
| note | 0.003 | medium-slow, session notes |
| episodic | 0.01 | events over weeks |
| working | 0.05 | fast, session context |
Formula: confidence_new = confidence_old * exp(-λ * days_since_modified_or_created)
Floor at 0.05.
Status transitions:
candidate → activeif confidence >= 0.5active → archivedif confidence < 0.2archived → archived(no resurrection)tombstonedskipped entirely
Dry-run on real data (851 records)
Status transitions:
active 708 → 760 (+52 from candidate promotion)
candidate 53 → 1 (−52)
archived 42 → 42
tombstoned 48 → 48 (skipped)
Archives triggered: 0 (oldest record is 105 days; even working λ=0.05 × 20d only decays ~64%)
Promotions: 52 (mostly procedural/semantic at conf=0.75-0.85 within last 36 days)
Confidence histogram (decayed subset, 803 records):
[0.20-0.40) = 93
[0.40-0.60) = 213
[0.60-0.80) = 286
[0.80-1.00] = 211
Edge cases found:
- 809/851 records missing time.modified (defaults to time.created) — handled
- No weird timestamps, no missing lifecycle blocks
- float-edge in exp(-λ*0) yields ~1.0 - 3e-13 (handled with tolerance)
Tests
20/20 PASS — all 10 spec test cases plus extras.
Sidecar status
$ curl -s http://127.0.0.1:4380/health | python3 -m json.tool
{
"status": "ok",
"qdrant": "reachable",
"ump": "reachable",
"ollama": "reachable",
"graph": {
"enabled": true,
"loaded": true,
"nodes": 2592,
"edges": 111,
"urns": 719,
"file": "/root/ump-recall/state/graph.json"
},
"qdrant_collection": "memories_ump",
"upstreams": {...},
"phase": "2C"
}
Sidecar restarted cleanly (old PID 4036490 killed, new PID 4102494 via background process).
Both gateways now use 3-channel RRF
Verified earlier:
- Hermes config patched (via terminal sed, since patch tool was security-guarded)
- Krystie config already pointing at shim
- MCP shim routes
recallto sidecar, falls back to canonical UMP if sidecar down - All other tools pass through to canonical UMP
What's still TODO
- Cron first run —
ump-decay-nightlyruns 2026-07-13 03:00. First dry-run before apply is the safety check. - Refresh
graph.jsonperiodically — when UMP gets new records, the graph is stale. Add a cron to runnode scripts/build_graph.jsweekly. - Filter noisy edges — some spurious relations like "hours→last" come from arrow regex matching English. Could add a stopword filter.
- Refine
entities.js— the graph layer added EXTRA_ENTITY_PATTERNS (lowercase services, port numbers, krystie-* codes) because the core extractor only knows a fixed allow-list. Could merge these into the main extractor. - Pre-existing MCP shim bug — shim tries to call canonical UMP subprocess without first running
initialize, soremember/revise/forget/get/feedbackall fail with "unknown tool". Out of scope here but flagged.