# How Memory Decay Is Calculated in ai-memory: A Deep Dive into the Retention Score Algorithm

> Discover how ai-memory calculates memory decay with the retention score algorithm. Learn about exponential decay, access frequency, and feedback adjustments for efficient memory management.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: deep-dive
- Published: 2026-09-06

---

**ai-memory calculates memory decay using a mathematically-driven retention score that combines exponential time decay, access frequency reinforcement, breadth-aware multipliers, and feedback-adjusted salience to determine when pages should be evicted from memory.**

The `akitaonrails/ai-memory` project implements a sophisticated decay system for managing long-term memory in AI systems. Unlike simple LRU caches, it uses tunable mathematical formulas that balance recency, usage patterns, collaborative breadth, and explicit user feedback. This article breaks down exactly how the decay calculation works, referencing the actual Rust source code in [`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs).

## Core Decay Parameters (DecayParams)

The foundation of memory decay calculation in ai-memory is the **`DecayParams`** struct, defined at lines 15-33 of [`decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/decay.rs). This configuration object controls the shape of every decay curve across the system.

| Field | Purpose | Default Value |
|-------|---------|---------------|
| `lambda` | Per-day exponential decay rate for time-since-update | `0.02` (~35-day half-life) |
| `sigma` | Weight multiplier for access reinforcement | `0.6` |
| `mu` | Per-day exponential decay rate for days-since-last-access | `0.04` |
| `salience_default` | Baseline salience without feedback | `1.0` |
| `cold_threshold` | Score cutoff for eviction candidacy | `0.3` |
| `hard_delete_after_days` | Tombstone lifetime before permanent removal | `90` |

These defaults are established in the `Default` implementation at lines 35-44. You can instantiate custom parameters to make memory decay faster or slower based on your use case.

```rust
use ai_memory_store::decay::DecayParams;

// Default conservative decay
let params = DecayParams::default();

// Aggressive decay: 10-day half-life, lower threshold
let aggressive = DecayParams {
    lambda: 0.07,        // ~10 day half-life
    cold_threshold: 0.5, // Evict sooner
    ..Default::default()
};

```

## The Retention Score Formula

The primary function **`retention_score`** (lines 48-75) computes whether a memory page survives another day. It accepts four inputs and delegates to the breadth-aware variant with breadth disabled.

### Input Parameters

- `age_days`: Days since the page was last updated
- `access_count`: Total historical searches that returned this page
- `days_since_access`: Optional days since most recent access
- `salience`: Optional explicit salience from user feedback

The function signature in [`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs):

```rust
pub fn retention_score(
    params: &DecayParams,
    age_days: f64,
    access_count: u32,
    days_since_access: Option<f64>,
    salience: Option<f64>,
) -> f64

```

At lines 66-73, this function simply calls `retention_score_with_breadth` with `distinct_actors = 0` and `breadth_weight = 0.0`, preserving backward compatibility with the original formula.

## Breadth-Aware Decay Extension

The full calculation lives in **`retention_score_with_breadth`** (lines 77-101). This extension adds a "distinct actors" factor so that pages read by many different operators decay more slowly—a key insight for collaborative knowledge systems.

### The Complete Formula

```rust
let time_term = salience * (-params.lambda * age_days).exp();
let breadth = 1.0 + breadth_weight * (f64::from(distinct_actors.max(1)) - 1.0).ln_1p();
let access_term = days_since_access.map_or(0.0, |d| {
    params.sigma * (1.0 + f64::from(access_count)).ln()
        * (-params.mu * d).exp()
        * breadth
});
time_term + access_term

```

### Component Breakdown

**Time term**: `salience × e^(-lambda × age_days)`

- Exponentially decays based on page age
- Scaled by salience (feedback-adjusted importance)
- With default `lambda = 0.02`, halves every ~35 days at base salience

**Access term**: Complex reinforcement based on usage patterns

- `ln(access_count + 1)`: Diminishing returns for repeated access
- `e^(-mu × days_since_access)`: Recent access matters more than old access
- Multiplied by `sigma` to control overall weight

**Breadth factor**: `1 + breadth_weight × ln(distinct_actors)`

- Default `breadth_weight = 0.0` disables this feature
- When enabled, collaborative pages (many distinct readers) resist decay
- Uses `ln_1p` for numerical stability at low actor counts

The final score is the sum of time and access terms. Pages scoring below `cold_threshold` become eviction candidates.

## Feedback-Driven Salience Adjustment

User feedback directly modifies how memory decay applies to specific pages. The **`salience_after_feedback`** function (lines 41-55) implements this closed-loop control.

### Feedback Impact Mapping

| Feedback Kind | Salience Change | Result |
|-------------|-----------------|--------|
| `Helpful` | Increase by 0.25 | Slower decay, longer retention |
| `NotHelpful` | Decrease by 0.25 | Faster decay, quicker eviction |
| `Stale` or `Wrong` | Set to `SALIENCE_MIN` (0.25) | Rapid decay, near-immediate candidate status |

The function enforces bounds: `SALIENCE_MIN = 0.25` and `SALIENCE_MAX = 2.0`. This means feedback can at most halve or double the time-term decay rate.

```rust
use ai_memory_core::FeedbackKind;
use ai_memory_store::decay::salience_after_feedback;

// Apply positive reinforcement
let boosted = salience_after_feedback(
    &params,
    Some(1.0),                  // Current salience
    FeedbackKind::Helpful
); // Returns 1.25

// Penalize outdated information
let penalized = salience_after_feedback(
    &params,
    Some(1.5),
    FeedbackKind::Stale
); // Returns 0.25 (minimum)

```

The updated salience feeds directly into subsequent `retention_score` calculations, creating a feedback loop between user judgments and automatic memory management.

## The Forget-Sweep Eviction Process

Decay calculations alone don't remove pages—a periodic **forget-sweep job** orchestrates actual eviction. This process spans multiple files in the codebase.

### Sweep Implementation Flow

1. **Query candidates**: `reader.decay_candidates` filters pages potentially below threshold
2. **Recompute scores**: Fresh retention scores using latest access statistics
3. **Soft delete**: `soft_delete_for_decay_if_latest` creates tombstone for confirmed candidates (score < `cold_threshold`)
4. **Hard delete**: `hard_delete_decayed_page_chain` permanently removes tombstones older than `hard_delete_after_days`

The sweep logic resides primarily in [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) and is invoked by [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs) as a background maintenance task.

## Practical Calculation Examples

### Example 1: Abandoned Page

```rust
use ai_memory_store::decay::{DecayParams, retention_score};

let params = DecayParams::default();

// Page: 60 days old, accessed 3 times, last access 45 days ago
let score = retention_score(&params, 60.0, 3, Some(45.0), None);
// Calculation: 1.0 * e^(-0.02*60) + 0.6 * ln(4) * e^(-0.04*45)
//             ≈ 0.301 + 0.6 * 1.386 * 0.165
//             ≈ 0.301 + 0.137 = 0.438

// Result: Above cold_threshold (0.3), survives this sweep

```

### Example 2: Actively Used Page with Breadth

```rust
use ai_memory_store::decay::retention_score_with_breadth;

// Same page, but with 5 distinct readers and breadth enabled
let score = retention_score_with_breadth(
    &params, 60.0, 3, Some(5.0), None, 5, 0.3
);
// Breadth factor: 1 + 0.3 * ln(5) ≈ 1.48
// Access term now multiplied by 1.48, significantly boosting retention

```

### Example 3: Negative Feedback Cascade

```rust
let params = DecayParams::default();

// Initial state: healthy page
let mut salience = Some(1.0);
let mut score = retention_score(&params, 30.0, 20, Some(2.0), salience);
// score ≈ 0.55 + 0.52 = 1.07 (well above threshold)

// Receive Wrong feedback
salience = Some(salience_after_feedback(&params, salience, FeedbackKind::Wrong));
// salience = 0.25

// Recalculate
score = retention_score(&params, 30.0, 20, Some(2.0), salience);
// score ≈ 0.138 + 0.13 = 0.268 (below threshold, eviction candidate)

```

## Key Files and Their Roles

| File Path | Functionality |
|-----------|---------------|
| [`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs) | Core decay mathematics: `DecayParams`, `retention_score`, `retention_score_with_breadth`, `salience_after_feedback` |
| [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) | Database operations: `soft_delete_for_decay_if_latest`, `hard_delete_decayed_page_chain`, `decay_candidates` query |
| [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs) | Orchestrates periodic background sweep using decay logic |
| [`crates/ai-memory-core/src/feedback.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/feedback.rs) | Defines `FeedbackKind` enum used in salience adjustment |

## Summary

- **Memory decay in ai-memory** is calculated through a tunable retention score combining exponential time decay, logarithmic access reinforcement, and optional breadth multipliers.
- The **`DecayParams`** struct in [`decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/decay.rs) provides six coefficients to customize decay curves without code changes.
- **`retention_score`** and **`retention_score_with_breadth`** implement the pure mathematical functions—no side effects, easy to test and reason about.
- **Feedback integration** via `salience_after_feedback` creates a closed loop where user judgments directly modify mathematical decay rates.
- **Eviction is two-phase**: soft-delete creates tombstones scored below `cold_threshold`, then hard-delete removes them after `hard_delete_after_days`.

## Frequently Asked Questions

### How does the lambda parameter affect memory decay?

The **`lambda`** parameter controls the exponential decay rate for the time-since-update term in the retention score formula. With the default value of `0.02`, a page loses half its time-term contribution approximately every 35 days. Increasing `lambda` to `0.07` shortens this half-life to roughly 10 days, making the system forget older information faster. This parameter directly scales the exponent in `e^(-lambda × age_days)` as implemented in [`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs) lines 89-90.

### Can memory decay be disabled entirely?

Complete disabling is not supported, but you can approximate it by setting extreme parameter values. Set `lambda` and `mu` to `0.0` to eliminate exponential decay, `sigma` to a large value to maximize access reinforcement, and `cold_threshold` to near zero. However, this configuration is untested and may cause unbounded memory growth. The system is designed for managed decay—consider adjusting `hard_delete_after_days` to a large value instead if you need extended retention.

### What happens when multiple users access the same page?

When **`breadth_weight`** is non-zero in `retention_score_with_breadth`, pages accessed by more distinct actors receive a multiplicative boost to their access term. The breadth factor `1 + breadth_weight × ln(distinct_actors)` means that purely collaborative pages resist decay proportionally to how widely they've been read. This implements the insight that broadly useful institutional knowledge should persist longer than personally relevant but niche information.

### How does feedback interact with the decay calculation?

Feedback modifies the **`salience`** variable that scales the time term in every retention score calculation. `Helpful` feedback increases salience by 0.25 (capped at 2.0), causing the page to decay more slowly. `NotHelpful` decreases it by 0.25 (floored at 0.25). `Stale` or `Wrong` feedback immediately drops salience to the minimum of 0.25, often pushing the page below `cold_threshold` and triggering eviction. This salience adjustment persists across all future decay calculations until new feedback arrives.