How the Breadth Term in ai-memory's Decay Formula Works

The breadth term measures how many distinct operators have accessed a page and multiplies the access component of the retention score by a log-scaled factor controlled by breadth_weight to preferentially retain team-wide knowledge.

In akitaonrails/ai-memory, the decay formula determines which pages survive eviction sweeps and which get purged. The breadth term—introduced in the retention_score_with_breadth function—adds a second-order signal that rewards pages accessed by many different actors rather than repeatedly by a single user. This guide explains the mathematical implementation, configuration options, and practical impact on memory retention.

Where the Breadth Term Lives

The core implementation resides in [crates/ai-memory-store/src/decay.rs](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs#L94-L118):

pub fn retention_score_with_breadth(
    params: &DecayParams,
    age_days: f64,
    access_count: u32,
    days_since_access: Option<f64>,
    salience: Option<f64>,
    distinct_actors: u32,
    breadth_weight: f64,
) -> f64 {
    // protect against invalid configuration
    let breadth_weight = if breadth_weight.is_finite() && breadth_weight >= 0.0 {
        breadth_weight
    } else {
        0.0
    };
    let time_term = salience * (-params.lambda * age_days).exp();

    // ---- breadth calculation ----
    // g(0) = g(1) = 1, then monotonic growth
    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
}

The Three Inputs That Control Breadth

1. distinct_actors

This is the count of unique operators who have accessed the page. If the per-actor tracking table does not exist (legacy pages), this value defaults to 0 or 1. The higher this number, the stronger the breadth signal—though with diminishing returns due to the logarithmic scaling.

2. breadth_weight (Configuration)

Set via the decay_breadth_weight configuration parameter. This is the dial that controls breadth influence:

  • 0.0 (default): The breadth term equals 1.0, making the formula identical to the historic retention_score. No migration cliff for existing deployments.
  • Positive values: Each additional actor contributes a multiplicative bonus to the access term. The ln_1p scaling prevents runaway scores from large teams.

3. The Mathematical Formula


breadth = 1 + breadth_weight * ln_1p(distinct_actors - 1)

Key properties:

  • ln_1p(x) computes ln(1 + x) with numerical stability for small values
  • When distinct_actors ≤ 1, breadth = 1 (neutral multiplier)
  • Growth is sublinear: each new actor adds less than the previous

Safety and Edge Case Handling

The implementation guards against misconfiguration in [decay.rs](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/decay.rs#L94-L118):

let breadth_weight = if breadth_weight.is_finite() && breadth_weight >= 0.0 {
    breadth_weight
} else {
    0.0
};

If breadth_weight is NaN, infinite, or negative, it is silently coerced to 0.0. This ensures the function never produces NaN scores or applies unexpected penalties during sweeps.

How Breadth Affects Eviction Decisions

The breadth multiplier applies only to the access term—the component shaped by access_count and days_since_access. The time_term (salience-weighted age decay) remains unchanged.

Practical implications:

  • Weight 0.0: Pages compete on raw access patterns alone. A page with 100 hits from one user scores higher than a page with 10 hits from 10 users.
  • Weight 1.5: The 10-user page receives a 1 + 1.5 * ln(10) ≈ 4.45x multiplier on its access term, potentially keeping it above the cold_threshold despite lower raw hits.

This design encodes the intuition that team-shared knowledge deserves stronger retention than personally hoarded information.

Where Breadth Scores Get Computed

Component File Purpose
Sweep job [sweep.rs](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-consolidate/src/sweep.rs) Evicts cold pages using retention_score_with_breadth
Breadth aggregation [reader.rs](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs#L3370-L3381) access_breadth_for_project counts distinct actors per page
Curator ranking [curator.rs](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-consolidate/src/curator.rs#L136) Manual review queue ranked by breadth-aware scores

The sweep job pulls breadth data through access_breadth_for_scoring (lines 3370-3381 in reader.rs), which joins the page table against per-actor access records to produce distinct_actors counts.

Working Code Example

use ai_memory_store::{DecayParams, retention_score_with_breadth};

let params = DecayParams::default();
let age_days = 30.0;
let access_count = 5;
let days_since_access = Some(3.0);
let salience = None;

// Scenario: 4 different team members accessed this page

// 1️⃣ Default weight — historic behavior, no breadth bonus
let score_no_breadth = retention_score_with_breadth(
    &params,
    age_days,
    access_count,
    days_since_access,
    salience,
    4,      // distinct_actors
    0.0,    // breadth_weight
);

// 2️⃣ Enable breadth with moderate weight — team knowledge bonus
let score_with_breadth = retention_score_with_breadth(
    &params,
    age_days,
    access_count,
    days_since_access,
    salience,
    4,
    1.5,
);

// breadth multiplier: 1 + 1.5 * ln(1 + 3) = 1 + 1.5 * 1.386 ≈ 3.08
// The access term roughly triples, helping the page survive eviction
println!("Weight 0.0: {score_no_breadth}");
println!("Weight 1.5: {score_with_breadth}");

Test-Driven Verification

The test suite in [access_breadth.rs](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/tests/suite/access_breadth.rs) validates the backward-compatibility guarantee:

#[test]
fn breadth_is_identity_at_the_default_weight() {
    let breadth_weight = 0.0;
    let actors = 5;
    let score = retention_score_with_breadth(
        &params, 30.0, 5, Some(3.0), None, actors, breadth_weight
    );
    // Identical to legacy formula when breadth is disabled
    assert_eq!(score, retention_score(&params, 30.0, 5, Some(3.0), None));
}

This test ensures that existing deployments can upgrade without reconfiguring and see no score changes until they explicitly opt in via decay_breadth_weight.

Summary

  • The breadth term in ai-memory's decay formula rewards pages accessed by many distinct operators, encoding team-wide utility into retention decisions
  • Mathematical foundation: breadth = 1 + breadth_weight * ln_1p(distinct_actors - 1) produces diminishing returns per additional actor
  • Default safety: breadth_weight = 0.0 yields exact backward compatibility with the historic retention_score function
  • Configuration: Set decay_breadth_weight to positive values to activate team-knowledge preferencing; invalid values are coerced to 0.0
  • Scope: Breadth multiplies only the access term (hit count and recency), not the time term (age decay)

Frequently Asked Questions

What happens if I don't configure decay_breadth_weight?

The default value is 0.0, which makes breadth = 1.0 for all pages. The retention_score_with_breadth function produces identical scores to the legacy retention_score function. Your existing pages compete for retention exactly as before.

Can the breadth term reduce a page's score below the legacy formula?

No. Because distinct_actors.max(1) ensures at least one actor and ln_1p of non-negative inputs is non-negative, the breadth multiplier is always ≥ 1.0 when breadth_weight ≥ 0. The term can only increase access-term scores or leave them unchanged.

How does ai-memory count distinct actors for legacy pages without per-actor tracking?

Pages created before the breadth table was introduced report distinct_actors as 0 or 1. The .max(1) call in the formula ensures these pages receive neutral treatment (breadth = 1.0) rather than penalties. You can backfill actor counts via the aggregation pipeline in reader.rs if desired.

What breadth_weight value strikes a good balance between individual and team utility?

The [access_breadth.rs](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/tests/suite/access_breadth.rs) test suite suggests evaluating 1.0 to 2.0 for moderate team preferencing. At 1.0, 10 distinct actors yield a 3.3x access-term multiplier—strong enough to matter without dominating age-based decay. Profile your specific access patterns before settling on a production value.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →