Status:
draft · Version v0.1 · Filed 2026-04-30spec_id: SPEC-059 version: v0.1 status: draft authored_by: Donna date: 2026-04-30
SPEC-059 — Tri-Graph Memory Activation + Memory-Hit Telemetry
Status
Draft — for Frank’s review. This is an execution spec, not a new design. SPEC-020 (Lola, 2026-04-19) already designed the Canonical / Semantic / Temporal tri-graph. ADR-018 already chose RRF as the fusion strategy. Neither shipped. SPEC-059’s job: ship them, and add the telemetry layer that makes the tri-graph’s value visible on the prism_vs_native dashboard.Origin
SPEC-058 review on 2026-04-30 (during the duplicate-signals diagnostic) surfaced a structural pattern:- Six SPEC-054 port misses in 24h. Cross-cutting Python invariants didn’t survive the Node port because nothing in the planning workflow forced them to surface. The dedup contract from SPEC-037 §3 was load-bearing for SPEC-054 but had no edge “SPEC-054 depends on SPEC-037” anywhere queryable.
- TODO #1 is still open — “Phase 4 adds Neo4j as the graph leg” — from ADR-018 RRF design. The graph leg has been on the queue since 2026-04-17.
- SPEC-020 v1.0 has been in DRAFT since 2026-04-19 — the full tri-graph design that nobody implemented.
- My own workflow miss (Donna, this session): went
git-first when reviewing the port misses. Frank stopped me. Once I queried memory, the answer was in the first result. Saved asfeedback_memory_first_for_historical.md. - Codex connection: Texi’s
observed_pending_duplicates: 2is the same family. Any agent — Codex, Cursor, future surfaces — runs into the same blind spot when the graph leg returns 0 results because edges don’t exist. Activation benefits all surfaces equally. - No memory-hit telemetry feeding the dashboard. Frank’s
project_prism_vs_native_telemetrystanding rule asked for observable proxies; we have qualitative tracking (“memory-saved-me events”) but nothing the dashboard can plot.
Three findings being addressed
- Graph leg empty. RRF (ADR-018) gives the graph leg equal weight; an empty leg silently degrades retrieval to two-graph.
- Temporal leg never built. SPEC-020 §5.3 designed
EntityStateversioning,HAS_STATE/SUPERSEDED_BYedges, andas_ofqueries. Recall today has no time awareness — a 6-month-old memory and a 1-day-old memory rank identically on relevance. - No memory-hit telemetry. No way to measure: hit rate, stale rate, tokens-loaded-vs-tokens-saved, per-agent recall behavior. The prism_vs_native question (“does memory pay for itself?”) cannot be answered with data today.
Goals
- Graph leg pays for itself. Spec ↔ spec dependency edges exist in Neo4j and contribute non-zero scores in
semantic_recall. - Temporal leg active.
as_ofqueries return point-in-time state; recency-decay is a default-on weight in fused ranking; stale memories surface for re-verification. - Memory tokenomics observable. Every
semantic_recallis logged; the dashboard plots hit rate, stale rate, tokens-loaded-vs-saved, per-agent breakdown. - Port-miss prevention. The graph leg surfaces upstream invariants (e.g., SPEC-037 §3) on the first recall when planning a port, so cross-language port misses cannot happen the way they did with SPEC-054.
Non-goals
- Replacing the current vector + lexical legs. Both stay. SPEC-059 adds graph + temporal, doesn’t subtract.
- Backfilling all 192+ historical memories into the new Temporal model. SPEC-020 §3 already declared this out-of-scope; SPEC-059 inherits.
- Solving entity disambiguation across ptypes (SPEC-020 §3 explicit out-of-scope).
- Cross-tenant fact conflicts (SPEC-056 covers identity isolation; this is its own future spec).
- Auto-resolving stale memories. SPEC-059 surfaces them; agents/operators decide whether to rewrite.
Architecture
§3.1 Graph leg activation
Implements SPEC-020 §5.1 + §5.2 with one significant addition: auto-extraction of spec-dependency edges from existing markdown sections. Edge auto-extraction. On everyprism_spec create | update, parse the body for these section headings (case-insensitive, level-2 or level-3):
Within each section, find tokens matching
SPEC-\d+ / ADR-\d+ / ADR #\d+. For each match, look up the target Entity in Neo4j (created lazily if absent) and write the edge from the current spec’s Entity. Edge properties: source_memory_id = the spec’s row id, extracted_from_section, created_at.
Backfill. Run the same extractor over every existing spec body in signal_queue-style migration: idempotent, re-runnable, writes new edges and prunes stale ones (where the markdown reference was removed in a later edit). Backfill is gated on a prism_audit --fixup memory-graph invocation so Frank can review the edge set before commit.
Recall integration. semantic_recall already returns a graph_score per result (visible in today’s output). After SPEC-059, the score reflects the count of edges connecting the result entity to entities mentioned in the query (1-hop and 2-hop, with diminishing returns). RRF (ADR-018) rolls graph_score into the final fused score equal-weight with vec_score and lex_score.
§3.2 Temporal leg activation
Implements SPEC-020 §5.3 with concrete schema, ingestion path, and recall integration. EntityState nodes.(:EntityState {uuid, entity_uuid, valid_from, valid_until, props_json, source_memory_id}). Every Entity (Spec, ADR, TODO, Persona, Project, etc.) has at least one EntityState — the current state — with valid_until = NULL. State transitions create a new EntityState and update the prior one’s valid_until.
Supersession edges. (:EntityState)-[:SUPERSEDED_BY {event_type, at, cause_memory_id}]->(:EntityState). event_type is descriptive: "renamed", "version_bumped", "superseded", "retired", "merged". The cause_memory_id points to the memory/ADR/delta that triggered the change — this is what makes event-causality queryable.
Ingestion. Existing verbs that mutate state (prism_spec update, prism_decide for status changes, prism_persona_create, prism_archive) become temporal-leg-aware: instead of UPDATE-in-place on the Postgres row, they write a new EntityState in Neo4j and update the prior one’s valid_until. Postgres remains the durability backstop with full state at any point in time; Neo4j carries the version chain.
Recall integration. Three new behaviors:
as_ofparameter onsemantic_recall—semantic_recall(query, as_of="2026-04-15T00:00:00Z")filters Temporal-layer reads to states valid at that timestamp. Defaultas_of=nullreturns current state (all states withvalid_until IS NULL).- Recency decay as a fourth RRF leg —
temporal_score = exp(-age_days / half_life), withhalf_life=180as default (memories from the last 6 months score ≥ 0.37; a year old ≥ 0.13). Fused with vec/lex/graph at equal weight by default; tunable per query class. - Stale-memory surfacing — recall results expose a
stalenessfield:last_verified_at,age_days,superseded(boolean). The agent can choose to re-verify before citing. New verbprism_remember --reverify <memory_id>updateslast_verified_atwithout changing content.
§3.3 Memory-hit telemetry
New observability layer; not in SPEC-020. Hooks into Frank’sproject_prism_vs_native_telemetry standing rule.
New table: memory_recall_events
semantic_recallwrites amemory_recall_eventsrow on every call.prism_remember update | rewritewrites amemory_citation_eventsrow withkind='rewrote'linking back to the recall that surfaced the now-rewritten memory (when the recall id is in scope).- New verb
prism_memory_citelets the agent explicitly tag a recall hit:prism_memory_cite(recall_event_id, memory_id, kind, notes). Agents call this at most once per relevant memory in a session — light enough to not be a tax. prism_checkpointandprism_wrapauto-aggregate citation events into the wrap delta (the existingtool_use_summarystanding rule).
query_text (regex + keyword list, no LLM) tags each recall as historical / architectural / current_state / decision. Lets the dashboard slice hit rate by query class. The four-class taxonomy comes from the workflow heuristic in feedback_memory_first_for_historical.md.
§3.4 Dashboard panels
Adds to the existing prism_server_dashboard (already indashboard/):
Files changed
No mcp-node WebSocket-plane changes. No SPEC-058 collision.
Test plan — port-miss prevention as the headline acceptance test
The test that would have prevented the 24h of port misses:- Canonical layer: every Entity has exactly one INSTANCE_OF
- Temporal layer: at most one EntityState with
valid_until IS NULLper Entity at any time - Supersession chain acyclic
valid_fromof superseder ==valid_untilof superseded
- Every
semantic_recallwrites exactly onememory_recall_eventsrow prism_memory_citewrites exactly onememory_citation_eventsrow- Dashboard SQL aggregates roll up correctly
Acceptance criteria
- All 47 existing specs have at least one outbound graph edge after backfill.
semantic_recall("SPEC-054 dependencies")returns SPEC-037 in top-5 — driven by graph_score, not just text similarity.semantic_recall(query, as_of="<past timestamp>")returns state valid at that timestamp; the same query withoutas_ofreturns current state.- Recency decay default-on: a 30-day-old session_delta scores higher than a 6-month-old session_delta when both have the same vec+lex match strength.
memory_recall_eventspopulated on every recall;memory_citation_eventspopulated on everyprism_memory_cite.- Dashboard
/memorypage renders all eight panels with non-zero data within 24h of merge. - The port-miss-prevention test (§test plan headline) passes against a fresh backend with the backfilled spec corpus.
- Codex (Texi) running the same
semantic_recall("SPEC-054 dependencies")query gets the same top-5 answer — the graph leg is surface-agnostic.
Phased rollout
Phase 1 — Graph leg. Backfill spec edges, wire auto-extractor onprism_spec create | update. RRF starts using graph_score on the existing data. Lowest-risk, highest-immediate-value: solves the SPEC-054 port-miss class on the next port.
Phase 2 — Temporal leg. EntityState versioning, SUPERSEDED_BY edges, as_of parameter, recency decay. Higher-risk because state-mutating verbs change shape.
Phase 3 — Telemetry + dashboard. memory_recall_events table, prism_memory_cite verb, eight dashboard panels. Lowest-risk addition; doesn’t change behavior, only observes.
Each phase ships independently. Phase 1 gating PR can land within days; Phase 2 is the longer pole; Phase 3 stacks on top of Phase 2 once telemetry has data to show.
Lessons learned (embedded — Frank asked for them in this spec)
The SPEC-058 diagnostic plus the SPEC-054 port-miss family teach four durable lessons. Saved as memory references where applicable:-
Memory-first for historical context. When the question is “how did this work before?” / “what’s the upstream invariant?” — query Prism memory first, fall back to git/SQL only when memory has been confirmed empty. SQL/SSH is right for live operational state, never for historical/architectural state. Saved as
feedback_memory_first_for_historical.md(this session, 2026-04-30). Frank caught this on me directly. -
Cross-cutting decorators don’t survive language ports unless the dependency graph forces them up. SPEC-037 §3’s dedup at the merge boundary was load-bearing for SPEC-054 but had no graph edge connecting the two specs. The port translated file-by-file and lost the invariant. Six SPEC-054 port misses in 24h are the receipt. Memory family:
project_spec_054_port_miss_*already documents the symptoms; SPEC-059’s graph leg is the structural prevention. - A tri-graph without graph edges is a two-graph. ADR-018 RRF gives equal weight to vector + lexical + graph; an empty graph leg silently degrades retrieval to two-graph and nobody noticed for ~2 weeks. Surface-level metrics (“recall returns results”) didn’t expose the degradation. Telemetry honesty (§3.3) is the prevention — without observability into per-leg lift, future degradations stay hidden the same way.
-
The Codex finding generalizes. Texi’s
observed_pending_duplicates: 2is the same family — a contract lost in port. The fix isn’t Codex-specific because the root cause isn’t Codex-specific; it’s “graph leg returns 0 because edges don’t exist, so no surface can find the upstream invariant.” All AI surfaces — Codex, Claude Desktop, Claude Code, Cursor, future ones — benefit equally from edge population. This is also why this spec is graph-first, surface-agnostic.
Out of scope
- Re-implementing memory ingestion from scratch. SPEC-020 §7 already designed it; SPEC-059 just adds extractor + telemetry hooks at the existing pipeline boundaries.
- Multi-tenant memory federation. SPEC-056 governs identity isolation; cross-tenant memory is a future spec.
- Auto-rewriting stale memories. SPEC-059 surfaces them; rewrite remains a deliberate agent/operator action.
- Token-cost normalization across surfaces (Codex tokens vs Claude tokens). The dashboard shows raw bytes; surface-relative pricing is dashboard-side calculation, deferred.
References
- Specs: SPEC-020 (Tri-Graph Knowledge Representation — Canonical/Semantic/Temporal, Lola 2026-04-19) · SPEC-058 (Signal Delivery Single-Source-of-Truth, this session) · SPEC-019 (env resolution) · SPEC-052 (per-identity signal cache) · SPEC-054 (Node MCP shim port — the regression class this spec prevents repeating).
- ADRs: ADR-018 (Reciprocal Rank Fusion), ADR-019 (memory candidates G5–G8), ADR-020/21/22 (proposed alongside SPEC-020).
- TODOs: #1 (Phase 4 adds Neo4j as the graph leg) — this spec closes it. #100 (SPEC-057 backend, blocked on SPEC-056 merge) — independent. #101 (graph-leg backfill — filed this session, this spec subsumes it).
- Memories:
feedback_memory_first_for_historical— the workflow lesson from this sessionproject_prism_vs_native_telemetry— the standing rule that motivates §3.3 telemetryproject_spec_054_port_miss_project_id+project_spec_054_port_miss_coalescing_reset— the regression family this spec preventsfeedback_document_port_misses— every cross-language port regression gets a memory; this spec means the graph leg surfaces the invariants before the portfeedback_no_hardcoded_machine_values+feedback_commercial_grade_naming— memory hygiene practices that the dashboard’s stale-rate panel can plot for compliance
- Live evidence (this session):
- SPEC-058’s diagnostic: 22 piggyback / 1 channels_push in last 24h proved a structural gap was hiding; memory wasn’t surfacing the upstream invariant
- The semantic_recall query in §test plan headline reproduces the gap on demand: today the graph_score for SPEC-054→SPEC-037 is 0; SPEC-059 makes it non-zero
Authorship
Donna (Claude Code, mini3, session 3f36b796). 2026-04-30. Authored after Frank’s directive to:- File the workflow miss (memory-first) — done as
feedback_memory_first_for_historical.md - File the graph-edge backfill TODO — done as TODO #101
- Write the spec covering graph leg + temporal leg + memory tokenomics — this spec
- Embed lessons learned, including the Codex generalization — see Lessons learned section above

