Decay, reflection, and reinforcement
How ranking evolves with age and usage — per-kind decay, episode consolidation into distilled facts, and usefulness-driven boosts.
Storing and retrieving memories is table stakes. What makes a memory corpus improve with time is a set of feedback loops: old noise fades from the top of results, clusters of related episodes consolidate into single durable facts, and records that prove useful in practice surface earlier. zero-memory implements all three as ranking-time mechanics on top of the same add-only store.
One invariant governs everything on this page: decay is demotion in
ranking, never invalidation of content. No memory dies of old age. Durable
kinds (gotcha, convention, decision, preference) decay slowly or not
at all, and there is no age-based auto-invalidation for any kind.
Decay by kind
A months-old episode should not outweigh a fresh convention, but until decay
existed, age barely influenced score. Now every kind has a half-life profile,
and search_memories (which also powers briefings) multiplies each
candidate's score by an exponential age factor derived from its kind:
episode— fast decay (a 30-day half-life);task/open-question— a 14-day half-life, deliberately aligned with the briefing's soft TTL (below);fact— a one-year half-life: facts go stale faster than judgments;gotcha— about three years;preference/reference— about five years;decision/convention— about ten years: the slow group, since a decision's why rarely expires on its own.
The multiplier is applied at ranking time, not as a filter. A direct query still finds an old episode; it just no longer crowds out fresher, more durable knowledge in top-k results.
Half-life profiles live in a ranking_config table in the database rather
than in application code, so ranking can be tuned without a release.
The table is readable by authenticated clients; writes go through migrations
or the service role only.
The fusion side of ranking is data too. The parameters of hybrid search — the
per-leg candidate pool size, the rank-fusion constant, and the leg weights —
live in a single-row fusion_config table under the same access rules. Values
change only after they beat the current ones on the retrieval-eval harness,
never ad hoc.
Soft TTL for open loops
Open loops surface oldest-first in briefings,
which is only safe if abandoned loops eventually stop resurfacing. Loops
older than a configurable threshold (default 14 days, via the
zm.open_loop_ttl_days database setting) drop out of the briefing's
open_loops section but remain live memories: they still appear in
recall, in the dashboard feed, and close_loop still works on them. The
briefing still says they exist: it counts the ones that sit on no card
(open_loops_aged) and names that count in one line.
Index hygiene
Decay and consolidation multiply invalidated rows over time, so the hot search indexes — the HNSW vector index and the full-text index — are partial indexes covering only non-invalidated rows. History grows; search does not feel it.
Seeing it
The insights dashboard has a "How it ages" section: median age of the live corpus, the faded share (records older than their kind's half-life, i.e. ranking weight below 0.5), and age buckets from a week to over a year.

Reflection: consolidating episodes
Related episodes accumulate around every piece of sustained work. Reflection turns each such cluster into one living fact:
- Detection runs inside the hygiene cycle, on the same triggers: scheduler
tick,
scan_hygiene, dashboard button. A cluster is a group of at least three episodes in one scope that share an entity and sit in a cosine band of "similar but not duplicates". The shared-entity requirement is load-bearing. Without it, the similarity band alone merges unrelated episodes into one blob. - Distillation. An LLM distiller condenses the cluster into a single
consolidated fact (kind chosen from content, typically
factorconvention) withderived_fromlinks to every source episode. - Approval. The distillate is never written automatically. It enters an approve / dismiss / snooze queue on the dashboard's reflections page; only an approved distillate becomes a memory.

Source episodes are not invalidated by approval. They fade naturally
under episode decay while the distillate — fresher and more complete — wins
ranking on merit. The derived_from edges keep full
provenance traceable.
Usage reinforcement
recall results that agents mark as actually useful generate a usefulness
signal (see recall quality). Reinforcement
wires that signal into ranking.
The hygiene cycle precomputes per-memory multipliers into a dedicated
memory_reinforcement table. Search reads only the multipliers; the raw
usage-event stream stays inaccessible to query paths. Directly reported
usefulness counts at full weight, while judge-attributed usefulness counts
weighted by its confidence (0.6 and up). Boosts follow a smoothed curve
capped at 1.4, so no memory can ride reinforcement to unbounded rank.
Facts promoted into standing rules are demoted (×0.6): a fact the agent
already receives through the rules channel does not need a high retrieval
rank too.
The signal has a negative half. An explicit challenge (see the
false-invalidation safeguards) counts at
full weight, and judge-attributed misled verdicts count weighted by
confidence (0.6 and up). The demotion mirrors the boost curve and is
floored at 0.5: a memory that misled its readers loses rank but is never
buried, because retirement must stay adjudicable through the review queue.
There is deliberately no demotion for surfaced-but-unused memories. Only
positive proof of use and explicit misled evidence move ranking, so mere
silence cannot bury knowledge.
Misled evidence also feeds a review trigger: a memory whose qualifying misled signals cross a threshold within the window is queued as a stale suspect, a single-subject dispute flagged for review, never auto-invalidated. A dismissed suspicion is only re-raised by evidence newer than the dismissal.
World-fact freshness
Decay ranks by age; freshness tracks something different — when a memory was
last checked against the world it describes. Only the fast-moving layer
participates, identified deterministically by scope and kind: core-scope
memories of kind fact or reference, whose truth lives outside the store
(library versions, ecosystem practice, API behavior). Conventions and
preferences never expire on a timer. Their oracle is you, not a document.
A fast-layer hit past its per-kind freshness budget (180 days for a fact, 90
for a reference; a never-checked memory counts from its own creation) carries
a stale_days field on recall. It is a marker, not a verdict: the fact still
stands, it just has not been re-checked lately. An agent that uses such a hit
is asked to verify it against current docs in passing, and either confirm it
(re-stamping the check) or write the corrected version with supersedes, or
challenge it.
External re-verification runs on the scheduled hygiene sweep, which re-checks a small, bounded batch of due fast-layer memories against the live web, using the model provider's server-side web search. A check has three honest outcomes. Current stamps the freshness ledger. Unverifiable stamps it too, so the web is not asked the same unanswerable question every night. Outdated raises a single-subject review dispute carrying what changed and the best supporting source; the check never supersedes a memory on its own, and an inconclusive (low-confidence) check records nothing at all. Owners whose configured provider has no server-side web search are skipped, and the skip itself is recorded: an unchecked record must look unchecked, never checked.
Curve shape, rollout, and where config lives
Reinforcement shipped last, behind a gate. Decay and reflection came first; the usefulness signal was allowed to accumulate for a long period before it influenced ranking, and wiring was validated on a held-out retrieval benchmark. Retrieval quality did not regress, and the share of reinforced records in top-k stayed low enough to preserve an exploration floor for new, not-yet-reinforced facts.
Decay is a pure exponential with no grace plateau. Long half-lives on protected kinds approximate a plateau where one is wanted, and a single curve keeps the ranking function analyzable.
Config lives in the database on purpose. Ranking constants embedded in SQL function bodies demonstrably drift when functions are recreated; a seeded config table read via join makes tuning explicit, auditable, and release-free.