Designing Memory That Forgets: The Math Behind Kroniqo's Decay Constant
Kroniqo's memory system uses exponential decay with λ=0.03. Why that number? What happens at 0.01 or 0.1? This is the reasoning behind a design decision that most agent frameworks skip entirely.
Most AI agent memory systems have the same flaw: they remember everything equally. A user preference stated six months ago carries the same weight as one stated this morning. A task completed last week competes with the current session for context window space. The result is an assistant that becomes progressively worse at being present.
Kroniqo uses a SQLite-backed Consequence Graph where every memory node carries a relevance score that decays over time. The decay constant is λ = 0.03, applied per hour. This post is about why — what the math says, what the alternatives look like, and where the model breaks down.
Why Forgetting Is a Feature
Human memory is not a lookup table. The hippocampus does not store events with equal weight and retrieve them on demand. Memory is reconstructive, associative, and time-sensitive — older memories fade unless reinforced, and recent experience dominates working context.
An agent with perfect recall is not more useful. It is noisier. If a user mentioned in January that they preferred shorter responses, but has spent the last three months asking for detailed technical explanations, the January preference is not ground truth — it is stale signal. A memory system that cannot distinguish stale from fresh will surface the wrong context at the wrong time.
Forgetting is the mechanism that keeps memory meaningful.
The design problem is calibration: how fast should memory fade? Too fast and the assistant loses continuity across sessions. Too slow and old context pollutes current reasoning. The decay constant is the parameter that sets this tradeoff.
The Model: Exponential Decay
The relevance of a memory node at time t hours after it was created:
relevance(t) = relevance₀ × e^(-λt)
Where:
relevance₀is the initial relevance score assigned at creation (0 to 1)λis the decay constant (0.03 per hour in Kroniqo)tis elapsed time in hourseis Euler's number (~2.718)
Exponential decay is the right model here for the same reason it describes radioactive decay and drug clearance: it is memoryless. The rate of forgetting at any moment depends only on the current relevance, not on how long the memory has existed. A memory that has survived two weeks is no more "protected" than a memory from yesterday — both decay at the same rate per hour.
This is a feature. A memory that has survived two weeks did so because it was reinforced or had high initial relevance. Its survival is already encoded in its current score, not in its age.
Half-Life: The Intuitive Unit
The half-life of a memory — the time for its relevance to drop to 50% — has a clean formula:
t½ = ln(2) / λ = 0.693 / λ
For λ = 0.03:
t½ = 0.693 / 0.03 = 23.1 hours
A memory in Kroniqo loses half its relevance in roughly one day. This is the number to reason about when choosing a decay constant. Half-life translates the abstract constant into something concrete: after one sleep cycle, what fraction of yesterday's context should still matter?
Three Constants, Three Behaviors
| λ | Half-life | After 8h | After 24h | After 72h | After 1 week |
|---|---|---|---|---|---|
| 0.01 | 69 hours | 92% | 79% | 49% | 11% |
| 0.03 | 23 hours | 79% | 49% | 11% | 0.1% |
| 0.10 | 7 hours | 45% | 9% | 0.07% | ~0% |
λ = 0.01 (too slow): After three days, nearly half the graph is still at near-original weight. A week-old conversation contributes 11% relevance — not negligible. For a daily-use assistant, this means last week's project context actively competes with this week's. The graph grows large and noisy. Retrieval surfaces stale memories the user has moved on from.
λ = 0.10 (too fast): After one sleep cycle (8 hours), a memory retains only 45% of its original weight. After 24 hours, it is effectively gone at 9%. The assistant loses continuity across sessions — a preference stated yesterday morning is nearly invisible by the next day. The system behaves like it has no long-term memory at all.
λ = 0.03 (chosen): After 8 hours, 79% relevance remains — within-session continuity is nearly perfect. After 24 hours, relevance is at 49% — still retrievable if directly relevant, but deweighted. After three days, 11% — background noise. After a week, the memory has effectively expired at 0.1%.
Why 0.03: The 24-Hour Argument
The key design constraint: the system should behave correctly across a sleep cycle.
A user interacts with Kroniqo in the evening. They go to sleep. They return the next morning. What should the agent remember?
- High-priority context: tasks, preferences, ongoing projects — should still be available
- Low-priority context: specific questions asked, intermediate reasoning steps — should have faded
- Completed tasks: should be mostly gone unless they have consequences
At λ = 0.03, after 24 hours:
High relevance₀ = 0.9 → relevance(24) = 0.9 × e^(-0.72) = 0.44
Low relevance₀ = 0.3 → relevance(24) = 0.3 × e^(-0.72) = 0.15
A high-initial-relevance memory survives the sleep cycle at 44% — above the retrieval threshold. A low-relevance memory drops to 15% — below it. The decay function is doing real work: it separates signal from noise using initial relevance as the discriminating variable.
This only works if initial relevance scores are well-calibrated. The decay constant does not fix bad initialization — it amplifies it. A memory incorrectly assigned high initial relevance will persist too long regardless of λ.
The second constraint: a week should be long enough to effectively clear a session.
At λ = 0.03:
relevance(168h) = relevance₀ × e^(-5.04) ≈ relevance₀ × 0.0065
After one week, even a memory with initial relevance 1.0 is at 0.65% — well below any practical retrieval threshold. The graph self-cleans on a weekly cycle without explicit deletion. This matters for storage: the database grows during active use and shrinks passively during dormant periods.
The Implementation: SQLite and Lazy Decay
The naive implementation applies decay on every write — recalculating all node scores whenever any memory is updated. This is O(n) per write for n nodes, which becomes expensive as the graph grows.
Kroniqo uses lazy evaluation instead. Each node stores:
CREATE TABLE memory_nodes (
id TEXT PRIMARY KEY,
content TEXT,
relevance₀ REAL, -- initial relevance at creation
created_at INTEGER, -- Unix timestamp (seconds)
last_access INTEGER
);Relevance is never stored directly. It is computed at retrieval time:
import math, time
def get_relevance(node, lambda_=0.03):
elapsed_hours = (time.time() - node.created_at) / 3600
return node.relevance0 * math.exp(-lambda_ * elapsed_hours)This means the database never contains decayed scores — it contains creation timestamps and initial scores. Decay is applied on read, not on write. The cost is one floating-point operation per retrieved node, paid at query time. The benefit is zero write overhead and a database that accurately reflects the current state of every memory on every read without any background maintenance jobs.
The tradeoff: if you need to query "all memories above relevance threshold X", you cannot use a SQL index on the relevance column — it does not exist as a stored value. Kroniqo solves this by fetching candidate nodes by recency (indexed on created_at) and then filtering by computed relevance in Python. For graph sizes under ~10,000 nodes this is fast enough; at scale it would need a materialized view refreshed periodically.
Session Compression at the 2-Hour Gap
A session boundary is detected when the gap between messages exceeds 2 hours. At this point, the current session's memory nodes are compressed before the session closes.
Compression does two things:
1. Merge low-delta nodes. Nodes whose content is semantically similar and whose relevance scores are close get merged into a single node with averaged relevance. This prevents the graph from accumulating near-duplicate memories from a single session.
2. Apply a session-end relevance adjustment. Nodes that were highly active during the session (accessed multiple times) get a small relevance boost before compression. Nodes that were created but never retrieved get a relevance penalty. The intuition: if a memory was relevant during the session itself, it is more likely to be relevant in the next one.
BOOST = 1.15 # 15% boost for accessed nodes
PENALTY = 0.85 # 15% penalty for never-accessed nodes
def compress_session(nodes, access_log):
accessed_ids = set(access_log)
for node in nodes:
if node.id in accessed_ids:
node.relevance0 = min(1.0, node.relevance0 * BOOST)
else:
node.relevance0 *= PENALTYThe 2-hour threshold is not a decay parameter — it is a session detection heuristic. A 2-hour gap in conversation is long enough to distinguish sessions reliably without false-positive splits during a focused working period with interruptions. It was chosen empirically, not derived.
What the Model Gets Wrong
Reinforcement is not modeled. If a user returns to a topic repeatedly across sessions, each mention creates a new node rather than reinforcing the existing one. The graph accumulates related nodes instead of strengthening a single one. The result: retrieval surfaces multiple similar memories instead of one high-confidence memory. A proper implementation would detect semantic overlap at write time and merge with a relevance boost.
Relevance₀ calibration is manual. The initial relevance assigned to each node is currently set by rule-based heuristics (task mentions get 0.8, casual conversation gets 0.3, etc.). These heuristics are wrong in edge cases. A user casually mentioning a severe constraint ("by the way I can't use Python") gets a low initial relevance, then decays below threshold before the constraint matters. The fix is learned initial relevance — but that requires labeled data on what users actually consider important.
Decay is uniform across categories. The same λ applies to a preference ("prefers short responses"), a fact ("lives in Nairobi"), and a task ("finish the CircuitLens writeup"). These have very different natural decay rates. Preferences should persist longer than tasks. Facts about the user's stable identity should barely decay at all. A category-aware decay system would use different λ values per memory type, but at the cost of more parameters to calibrate.
The 2-hour boundary is fragile. A user who takes a 2.5-hour break mid-task will trigger session compression in the middle of ongoing context. The session boundary heuristic should probably incorporate task state — is there an open task? — rather than relying purely on time gap.
λ = 0.03 is not a magic number. It is the result of asking "what should memory look like after one sleep cycle?" and working backwards from a half-life of 23 hours. The constant will need tuning as Kroniqo moves from single-user to multi-session production use — different usage patterns will have different natural memory timescales.
But the model — exponential decay, lazy evaluation, session-boundary compression, initial relevance as the discriminating variable — is the right structure. The constant is a dial. The structure is the design.
Kroniqo source: Jeff9497/Kroniq09. Memory module: memory/consequence_graph.py.