| adr | 211 | |||||||
|---|---|---|---|---|---|---|---|---|
| title | Temporal Coherence Decay for Agent Memory Retrieval | |||||||
| status | accepted | |||||||
| date | 2026-06-13 | |||||||
| authors |
|
|||||||
| related |
|
|||||||
| tags |
|
Accepted. Implemented on branch research/nightly/2026-06-13-temporal-coherence-agent-memory
as crates/ruvector-temporal-coherence. All 21 unit tests pass; all 4 acceptance
tests pass with cargo run --release -p ruvector-temporal-coherence --bin tcd-benchmark.
As AI agents accumulate memories over time, two problems emerge:
-
Recency blindness: Pure cosine similarity treats a memory from last week identically to one from three months ago. For an agent operating in a changing world, recent memories often carry more actionable signal.
-
Coherence dilution: Isolated memories — those without strong semantic neighbours in the memory corpus — may represent one-off observations rather than stable world knowledge. A memory that is reinforced by many similar memories across the corpus is statistically more reliable.
Neither problem is addressed by existing RuVector search primitives (HNSW,
IVF, filtered-ANN). This ADR introduces ruvector-temporal-coherence, which
adds temporal decay and graph-coherence gating as first-class scoring signals
in agent memory retrieval.
The design is inspired by:
- Governing Evolving Memory in LLM Agents (SSGM, arXiv 2603.11768, 2026)
- Temporal Tensor Compression work already in
ruvector-temporal-tensor - MinCut coherence gating already in
ruvector-mincut/ruvector-coherence
Ship crates/ruvector-temporal-coherence as a standalone crate providing three
scored retrieval variants over an append-only agent memory store:
| Variant | Scoring formula | Primary fitness metric |
|---|---|---|
FlatSearch |
cosine_sim(q, m) |
Cosine recall@K |
TemporalSearch |
cosine_sim × exp(-λ·age) |
Mean recency of results |
CoherenceSearch |
cosine_sim × ((1-w)·decay + w·gate) |
Mean coherence gate of results |
Where gate(m) = degree(m) / max_degree over the adjacency graph of
memories whose pairwise cosine similarity exceeds coherence_threshold.
The trait surface is:
pub trait VectorSearch {
fn search(&self, query: &[f32], k: usize, store: &MemoryStore) -> Vec<SearchResult>;
}All three variants implement VectorSearch. DecayConfig carries the
exponential decay parameter. CoherenceGraph wraps the adjacency degree
array and is built once at indexing time.
- Agents can tune retrieval by passing a
DecayConfigandCoherenceGraphwithout changing query code — theVectorSearchtrait is uniform. - Temporal decay is a pure multiply on top of cosine scan — no extra I/O, no graph traversal per query.
- Coherence gate overhead is O(1) per candidate (single array lookup).
- The coherence graph build is one-time (O(n²) at indexing) — in production
this would be replaced by an approximate k-NN graph via HNSW from
ruvector-acornorruvector-core, reducing build to O(n·log n). - MCP memory tools can expose
DecayConfigas a tool parameter, enabling ruFlo workflow loops to passhalf_lifeas a session-scoped configuration.
- The O(n²) coherence graph build limits PoC to ~50K memories without HNSW approximation. This is documented and the production migration path is clear.
- The exponential decay half-life is a hyperparameter that must be tuned per domain. A universal default (30% of session time) is provided but may need calibration.
- Coherence gate is based on pairwise cosine threshold — not mincut. A future
upgrade (see open questions) should replace the degree-normalised gate with a
proper spectral coherence score from
ruvector-coherence::spectral.
SOTA diversity reranking with a deterministic greedy algorithm. Scored 4.50 by the nightly selection formula. Rejected for this run because it operates post-hoc on existing cosine results rather than integrating temporal and coherence signals into the scoring pass — a structurally different problem. Recommended as the next nightly topic.
2-bit sign-magnitude encoding for HNSW topology. Scored 4.45. Rejected because it targets index construction speed, not agent memory retrieval fitness — a different layer of the stack.
Scored 4.05. Rejected for now because it depends on the coherence graph
structure being built first — logically downstream of this ADR. Should be
built on top of ruvector-temporal-coherence in a future nightly.
-
crates/ruvector-temporal-coherence— FlatSearch, TemporalSearch, CoherenceSearch -
DecayConfigwithNone,Linear,Exponentialvariants -
CoherenceGraphwith threshold-gated adjacency degree - Benchmark binary with per-variant fitness metrics
- 21 unit tests, 4 acceptance tests, all green
- Replace O(n²) graph build with approximate k-NN from
ruvector-acorn - Add
spectralcoherence gate fromruvector-coherenceas optional feature - Expose
DecayConfigas MCP tool parameter inmcp-brain-server - Integrate with
ruvector-snapshotfor RVF-packed memory checkpoints
- Learned half-life: train λ per agent session from outcome feedback
- Graph-coherence mincut gating: replace degree normalisation with spectral Fiedler value to identify genuine coherence domains
- Drift detection: flag memories whose coherence drops below threshold after corpus updates (connects to SSGM arXiv 2603.11768)
Hardware: x86_64 Linux 6.18.5, Intel Celeron N4020
Rust: 1.94.1 (e408947bf 2026-03-25)
Command: cargo run --release -p ruvector-temporal-coherence --bin tcd-benchmark
Dataset: N=5000, D=128, K=10, 200 queries, 20 clusters, half_life=300 000
| Variant | Mean µs | p50 µs | p95 µs | Throughput | Memory | Fitness metric | Acceptance |
|---|---|---|---|---|---|---|---|
| FlatSearch | 1 036 | 1 017 | 1 136 | 965 q/s | 2 656 KB | cosine_recall=1.000 | PASS |
| TemporalSearch | 1 033 | 1 020 | 1 096 | 967 q/s | 2 656 KB | recency=0.962 | PASS |
| CoherenceSearch | 1 070 | 1 053 | 1 179 | 935 q/s | 2 675 KB | coh_gate=0.971 | PASS |
Coherence graph build: 1 996 ms, 590 313 edges (dense at threshold=0.55, random corpus). Production corpora will be sparser — 10–50× fewer edges expected.
- Wrong half-life: λ too large → retrieves only the very latest memories, missing important older context. Mitigation: expose half-life in MCP tool and instrument per-session feedback loops.
- Dense coherence graph: High-overlap corpora (e.g., duplicate-heavy logs)
produce near-uniform gate values, eliminating coherence signal. Mitigation:
dedup before building the coherence graph, or raise
coherence_threshold. - Clock skew: If timestamps are not monotonic (e.g., agent memory ingested
from an external replay), the decay formula produces incorrect ordering.
Mitigation: enforce strictly monotonic ingestion timestamps in
MemoryStore. - Negative cosine scores: When cosine_sim < 0 and the temporal factor > 0,
TemporalSearchscores stay negative — they are still correctly ranked below positive-scoring memories. Acceptance test verifies scores >= -0.01.
- Memory content is stored as raw
f32vectors — no PII in the vector layer. MemoryMetadata.sourceis a string field; callers must sanitise before inserting from untrusted origins.- Coherence graph edges reveal which memories are semantically similar to which; in multi-tenant deployments the coherence graph must be per-tenant.
- Existing code using
ruvector-corecosine scan can wrap results withFlatSearch— identical behaviour, no migration required. - To enable temporal decay: construct
DecayConfig::exponential(now, half_life)and swapFlatSearch→TemporalSearch. - To enable coherence gating: build
CoherenceGraph::build(&store, threshold)once at session start, then swap toCoherenceSearch::new(decay, graph, w). - The production upgrade path replaces the O(n²) graph build with
ruvector-acornapproximate k-NN construction — theCoherenceGraphAPI is unchanged.
- What is the right default
coherence_weight(currently 0.30)? Should it be calibrated per domain or per agent session? - Should
CoherenceGraphstore the full adjacency list or just the degree array? Full adjacency enables edge-level mincut pruning but costs O(n·deg) RAM. - Is exponential decay the right family? SSGM uses Weibull decay (two-parameter)
— should
DecayKindadd aWeibullvariant? - Should the coherence gate be computed against the full corpus or only against the memories in the current query's temporal window?