# 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 `Graph` from `./graph.js` - Add lazy-loaded singleton `getGraph()` that loads from `GRAPH_FILE` (env-overridable) - Add `runGraphChannel()`: extracts entities from query, BFS up to `GRAPH_DEPTH` hops (default 2), score = 1/hops - Wire 3rd channel into `/recall`: `Promise.allSettled([runUmpChannel, runVectorChannel, runGraphChannel])` - Update `/health` to report graph stats (`{enabled, loaded, nodes, edges, urns, file}`) and bump phase label to `2C` - 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 → active` if confidence >= 0.5 - `active → archived` if confidence < 0.2 - `archived → archived` (no resurrection) - `tombstoned` skipped 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 `recall` to sidecar, falls back to canonical UMP if sidecar down - All other tools pass through to canonical UMP --- ## What's still TODO 1. **Cron first run** — `ump-decay-nightly` runs 2026-07-13 03:00. First dry-run before apply is the safety check. 2. **Refresh `graph.json` periodically** — when UMP gets new records, the graph is stale. Add a cron to run `node scripts/build_graph.js` weekly. 3. **Filter noisy edges** — some spurious relations like "hours→last" come from arrow regex matching English. Could add a stopword filter. 4. **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. 5. **Pre-existing MCP shim bug** — shim tries to call canonical UMP subprocess without first running `initialize`, so `remember`/`revise`/`forget`/`get`/`feedback` all fail with "unknown tool". Out of scope here but flagged.