# How ai-memory Implements Decay Math and Salience-Scaled Retention for Forget-Sweep

> Discover how ai-memory uses decay math and salience-scaled retention to optimize forget-sweep. Learn how episodic pages survive based on age, access, feedback, and readership.

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

---

**ai-memory calculates retention scores using exponential decay formulas that combine page age, access frequency, operator feedback, and breadth of readership to determine which episodic pages survive the periodic forget-sweep.**

The `akitaonrails/ai-memory` repository implements a deterministic, mathematically-grounded approach to knowledge eviction in its long-term memory store. The algorithm lives primarily in [`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs), where pure functions compute retention scores without hidden state—making the sweep fully testable and predictable.

## Core Decay Parameters in DecayParams

The `DecayParams` struct defines six coefficients that shape the retention curve. These defaults are tuned for roughly 35-day half-life on aging content:

| Field | Default | Purpose |
|-------|---------|---------|
| `lambda` | 0.02 | Exponential decay per day for page age |
| `sigma` | 0.6 | Magnitude of access-reinforcement term |
| `mu` | 0.04 | Decay per day for days-since-last-access |
| `salience_default` | 1.0 | Fallback when no operator feedback exists |
| `cold_threshold` | 0.5 | Score floor for eviction candidacy |
| `hard_delete_after_days` | 30 | Tombstone lifetime before permanent removal |

The `Default` implementation for `DecayParams` lives at lines 15-45 of [`crates/ai-memory-store/src/decay.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs). All parameters are `pub` and can be overridden at store initialization or via the MCP server admin API.

## The Retention Score Formula

### Basic Score: retention_score()

The entry point `retention_score()` (lines 48-75) accepts age, access count, days since access, and optional salience. It delegates to `retention_score_with_breadth()` with `breadth_weight: 0.0`, preserving backward compatibility with the historical formula.

### Extended Score: retention_score_with_breadth()

Lines 94-119 contain the full computation. The final score sums two terms:

**Time term:** `salience · exp(-λ · age_days)`

**Access term:** `σ · ln(1 + access_count) · exp(-μ · days_since_access) · breadth`

The **breadth factor** rewards pages read by distinct operators: `1 + breadth_weight · ln(1 + distinct_actors - 1)`. This prevents "siloed" pages—those accessed many times by one operator—from gaming the access term.

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

let params = DecayParams::default();
let score = retention_score(
    &params,
    120.0,           // age_days: 4 months old
    35,              // access_count
    Some(3.0),       // days_since_access: recent hit
    None,            // no explicit salience
);
// Score combines: (1.0 · e^(-2.4)) + (0.6 · ln(36) · e^(-0.12))

```

## Salience Scaling via Operator Feedback

The `salience_after_feedback()` function (lines 40-55) adjusts a page's retention multiplier based on operator judgments. The implementation uses hard bounds and discrete steps:

- **SALIENCE_MIN**: 0.25 (floor for Stale/Wrong feedback)
- **SALIENCE_MAX**: 2.0 (ceiling)
- **SALIENCE_STEP**: 0.25 (increment/decrement for Helpful/NotHelpful)

Stale or Wrong feedback immediately drops salience to 0.25. Helpful increments by 0.25; NotHelpful decrements by 0.25.

```rust
use ai_memory_store::decay::{DecayParams, salience_after_feedback};
use ai_memory_core::FeedbackKind;

let params = DecayParams::default();
let boosted = salience_after_feedback(&params, None, FeedbackKind::Helpful);
// Returns 1.25: default 1.0 + SALIENCE_STEP

let penalized = salience_after_feedback(&params, Some(1.5), FeedbackKind::Wrong);
// Returns 0.25: immediate floor

```

The feedback system creates a **closed-loop decay model**: operator engagement directly modulates the mathematical survival curve.

## The Four-Stage Forget-Sweep Flow

### Stage 1: Candidate Selection

`Reader::decay_candidates()` (lines 3183-3218 in [`reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/reader.rs)) queries the `pages` table and filters:

- Computed retention score below `cold_threshold`
- `pinned = false` (pinned pages are decay-immune)
- Not already superseded

The query applies the `retention_score` formula in SQL or loads rows for Rust-side scoring depending on index availability.

### Stage 2: Soft Delete

For each candidate, `Ops::soft_delete_for_decay_if_latest` (lines 1655-1720 in [`ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/ops.rs)) marks:

- `superseded_at = NOW()` on the page record
- Creates a decay tombstone entry tracking the deletion reason

Soft deletion preserves version history and allows potential resurrection if the page is re-imported.

### Stage 3: Hard Delete

After `hard_delete_after_days` elapse, `Reader::decay_tombstones_before()` lists expired tombstones. `Ops::hard_delete_decayed_page_chain` (lines 1800-1885) recursively removes:

- The tombstone record
- All version ancestry from the database
- Associated wiki files on disk

### Stage 4: Pinning Protection

Pages can be manually pinned via CLI or API. The `pinned` boolean is checked in every sweep query, providing a simple override for critical knowledge.

```rust
use ai_memory_store::Store;

let store = Store::open("data/")?;
store.run_forget_sweep(&DecayParams::default())?;
// Executes full pipeline: candidates → soft delete → hard delete cleanup

```

## Orchestration by the Consolidator

The sweep is not triggered automatically by the store. Instead, [`crates/ai-memory-consolidate/src/sweep.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-consolidate/src/sweep.rs) implements a periodic job that:

1. Fetches `decay_candidates()` from the store
2. Optionally applies a configured `decay_breadth_weight` (via `ServerBuilder::with_decay_breadth_weight`)
3. Invokes the soft/hard delete operations
4. Logs metrics on pages evicted vs. retained

This separation of concerns allows the store to remain stateless while the consolidator handles scheduling and observability.

## Pure-Functional Design Benefits

The decay implementation avoids hidden state:

- `retention_score_with_breadth` takes only parameters—it does not query the database
- `salience_after_feedback` computes deterministically from inputs
- Test suites exercise edge cases: ancient pages (large `age_days`), recent access spikes, boundary salience values, and breadth-weight variations

This design ensures that **identical inputs always produce identical scores**, making the forget-sweep behavior reproducible across environments.

## Summary

- **DecayParams** configures six coefficients (λ, σ, μ, thresholds) governing the retention curve
- **retention_score_with_breadth** computes survival probability from age, access history, recency, salience, and operator breadth
- **salience_after_feedback** implements bounded, stepped salience adjustments based on operator judgments
- **Four-stage sweep**: candidate selection → soft delete → tombstone aging → hard delete
- **Pinning** provides manual override for decay immunity
- **Pure functions** make the algorithm testable and deterministic

## Frequently Asked Questions

### How does ai-memory balance age decay against access frequency?

The retention formula uses a **sum of two exponential terms**. Age-driven decay (`exp(-λ · age_days)`) continuously erodes the salience-weighted base score, while access history (`ln(1 + access_count)`) provides multiplicative reinforcement that itself undergoes recency decay (`exp(-μ · days_since_access)`). A page with massive historical access but no recent hits still decays—the access term's half-life is roughly 17 days with default μ=0.04.

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

The **breadth factor** in `retention_score_with_breadth` rewards distribution across distinct actors. With positive `breadth_weight`, a page read once each by five operators scores higher than a page read five times by one operator—even with identical total access counts. This prevents "echo chamber" retention where single-user obsessive access preserves niche content.

### Can the decay parameters be changed at runtime?

Yes. The MCP server in [`crates/ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/server.rs) exposes `DecayParams` through its admin API. `ServerBuilder::with_decay_breadth_weight` specifically allows tuning the breadth multiplier without code changes. However, parameter changes only affect **future** sweep calculations—already-computed scores in running systems are not retroactively adjusted.

### Why does ai-memory use soft deletion before hard deletion?

The **two-phase eviction** supports operational safety and potential recovery. Soft deletion (setting `superseded_at`) immediately removes the page from search results while preserving data. If the page is re-ingested from an external source, the system can detect version ancestry. The `hard_delete_after_days` delay (default 30 days) provides a window for administrative intervention before permanent removal.