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 equals1.0, making the formula identical to the historicretention_score. No migration cliff for existing deployments.- Positive values: Each additional actor contributes a multiplicative bonus to the access term. The
ln_1pscaling prevents runaway scores from large teams.
3. The Mathematical Formula
breadth = 1 + breadth_weight * ln_1p(distinct_actors - 1)
Key properties:
ln_1p(x)computesln(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 a1 + 1.5 * ln(10) ≈ 4.45xmultiplier on its access term, potentially keeping it above thecold_thresholddespite 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(
¶ms,
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(
¶ms,
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(
¶ms, 30.0, 5, Some(3.0), None, actors, breadth_weight
);
// Identical to legacy formula when breadth is disabled
assert_eq!(score, retention_score(¶ms, 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.0yields exact backward compatibility with the historicretention_scorefunction - Configuration: Set
decay_breadth_weightto positive values to activate team-knowledge preferencing; invalid values are coerced to0.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →