# Decay Retention Formula in ai-memory-store: How It Calculates Page Eviction Scores

> Understand the decay retention formula in ai-memory-store. Learn how it calculates page eviction scores using salience and access frequency to optimize memory.

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

---

**The decay retention formula in ai-memory-store computes a page's retention score by combining a time-decayed salience term with an access-frequency term, evicting pages that fall below a configurable cold threshold.**

The `ai-memory` repository (akitaonrails/ai-memory) implements a deterministic forgetting mechanism in its storage layer to manage memory retention. The core logic resides in **[`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs)**, which provides pure mathematical functions that determine whether a stored page should be kept or evicted based on age, access patterns, and optional breadth metrics.

## The Mathematical Formula

The retention formula calculates a final score as the sum of two components: a **time term** representing intrinsic value decay and an **access term** representing usage reinforcement.

```text
score = time_term + access_term

```

Pages with scores below `params.cold_threshold` (default **0.20**) become candidates for eviction.

### Time Term (Salience Decay)

The time term models how a page's intrinsic **salience** decays exponentially over calendar age:

```rust
time_term = salience * exp(-λ * age_days)

```

- **`λ` (`params.lambda`)**: The per-day exponential decay rate, defaulting to **0.02** (approximately a 35-day half-life).
- **`salience`**: The page's explicit salience value, or `params.salience_default` (1.0) when not specified.
- **`age_days`**: The page's age in days since creation or last major update.

### Access Term (Frequency and Recency)

The access term rewards pages with high access counts while penalizing stale access patterns:

```rust
access_term = σ * ln(1 + access_count) * exp(-μ * days_since_access) * breadth

```

- **`σ` (`params.sigma`)**: The magnitude of the access-reinforcement boost.
- **`access_count`**: Total historical search hits for the page.
- **`μ` (`params.mu`)**: The per-day decay rate applied to the age of the last access, defaulting to **0.04**.
- **`days_since_access`**: Days elapsed since the page was last accessed.
- **`breadth`**: An optional multiplier for distinct actor counts (see below).

### Breadth Multiplier (Optional)

When tracking distinct actors, the system applies a breadth scaling factor:

```rust
breadth = 1.0 + breadth_weight * ln(1 + (distinct_actors.max(1) - 1))

```

By default, `breadth_weight` is **0.0**, disabling this feature. When enabled (e.g., `breadth_weight = 0.5`), pages accessed by multiple distinct operators receive a logarithmic boost to their retention scores.

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

The source file defines two public entry points and a configuration struct:

- **`retention_score`**: The standard interface (lines 48–75) that calls the breadth-aware variant with `distinct_actors = 0` and `breadth_weight = 0.0`.
- **`retention_score_with_breadth`**: The full implementation (lines 94–119) that accepts actor diversity parameters.
- **`DecayParams`**: A configuration struct (lines 15–33) bundling all formula constants with a `Default` implementation (lines 35–45) that sets λ = 0.02, σ = 0.6, μ = 0.04, and `cold_threshold` = 0.20.

All functions are **pure**—they depend only on their inputs and produce deterministic outputs—making them fully unit-tested in the `#[cfg(test)]` module at the bottom of the file.

## Code Examples

### Basic Retention Scoring

Calculate scores using the default parameter set:

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

fn main() {
    // Default parameters (λ = 0.02, σ = 0.6, μ = 0.04)
    let params = DecayParams::default();

    // Fresh page with no access history
    let score = retention_score(
        &params,
        0.0,    // age_days
        0,      // access_count
        None,   // days_since_access
        None    // salience
    );
    println!("Fresh page score ≈ {score:.3}"); // ≈ 1.0

    // Old page (200 days) with many recent hits
    let score = retention_score(
        &params,
        200.0,          // age_days
        50,             // access_count
        Some(2.0),      // days_since_access
        None            // salience
    );
    println!("Hot old page score ≈ {score:.3}");
}

```

### Scoring with Actor Breadth

Include distinct actor counts for collaborative filtering scenarios:

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

fn main() {
    let params = DecayParams::default();

    // Page accessed by 10 distinct operators with breadth weighting
    let score = retention_score_with_breadth(
        &params,
        30.0,       // age_days
        20,         // access_count
        Some(1.0),  // days_since_access
        None,       // salience
        10,         // distinct_actors
        0.5         // breadth_weight
    );
    println!("Score with breadth ≈ {score:.3}");
}

```

## Integration with the Eviction Pipeline

The decay functions integrate into the storage layer through several coordinated components:

- **[`crates/ai-memory-store/src/writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs)**: Contains the forgetting-sweep job that iterates over stored rows, invokes `retention_score`, and marks low-scoring pages for eviction.
- **[`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs)**: Provides high-level API operations that rely on the decay logic to make eviction decisions during memory pressure events.
- **[`crates/ai-memory-store/migrations/V03__decay.sql`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/migrations/V03__decay.sql)**: Database migration adding the `access_count` and `last_accessed_at` columns required by the formula.

This architecture separates the pure mathematical policy (in [`decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/decay.rs)) from the imperative eviction mechanics (in [`writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/writer.rs)), allowing the retention strategy to be tested and tuned independently of database operations.

## Summary

- The **decay retention formula** combines exponential time decay with logarithmic access frequency to produce a deterministic retention score.
- **Two public functions**—`retention_score` and `retention_score_with_breadth`—implement the logic in [`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs).
- **Configurable parameters** (`DecayParams`) control decay rates (λ, μ), access boost magnitude (σ), and the eviction threshold (default 0.20).
- **Pure functional design** ensures deterministic, testable behavior without side effects.
- The **breadth multiplier** optionally rewards pages accessed by multiple distinct actors when `breadth_weight > 0.0`.

## Frequently Asked Questions

### What is the default half-life for page salience?

With the default `lambda` value of **0.02**, the time-decay component has an approximate half-life of **35 days**. This means a page's intrinsic salience decays to 50% of its original value roughly five weeks after creation if never accessed.

### How does the breadth weight affect retention scores?

The `breadth_weight` parameter (default **0.0**) scales the impact of distinct actor counts. When set above zero, the formula applies a logarithmic multiplier `ln(1 + distinct_actors - 1)` to the access term, meaning pages touched by multiple operators receive higher retention scores than pages accessed by a single operator with the same frequency.

### What happens when a page's score drops below the cold threshold?

Pages scoring below `params.cold_threshold` (default **0.20**) become candidates for eviction during the next forgetting-sweep cycle executed by **[`writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/writer.rs)**. The sweep job periodically queries stored pages, recalculates their scores using current timestamps and access statistics, and removes entries that fail to exceed the threshold.

### Where are the decay parameters configured?

All parameters are bundled in the **`DecayParams`** struct defined in [`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs) (lines 15–33). The system uses the `Default` trait implementation for standard values, but instances can be constructed programmatically with custom values for `lambda`, `sigma`, `mu`, `salience_default`, `cold_threshold`, and other tuning constants.