# Memory Lifecycle — Write, Age, Decay, Forget

> How a memory is born (mem::remember), enriched with concepts and graph edges, ages through hot/warm/cold retention tiers via exponential decay, and is eventually evicted or crystallized into long-term patterns — all driven by access frequency, TTL, and consolidation pipelines.

- Repository: rohitg00/agentmemory
- GitHub: https://github.com/rohitg00/agentmemory
- Human wiki: https://grok-wiki.com/public/wiki/rohitg00-agentmemory-94f173bce1dc
- Complete Markdown: https://grok-wiki.com/public/wiki/rohitg00-agentmemory-94f173bce1dc/llms-full.txt

## Source Files

- `src/functions/remember.ts`
- `src/functions/retention.ts`
- `src/functions/evict.ts`
- `src/functions/consolidation-pipeline.ts`
- `src/functions/crystallize.ts`
- `src/functions/auto-forget.ts`
- `src/functions/access-tracker.ts`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [src/functions/remember.ts](src/functions/remember.ts)
- [src/functions/retention.ts](src/functions/retention.ts)
- [src/functions/evict.ts](src/functions/evict.ts)
- [src/functions/consolidation-pipeline.ts](src/functions/consolidation-pipeline.ts)
- [src/functions/crystallize.ts](src/functions/crystallize.ts)
- [src/functions/auto-forget.ts](src/functions/auto-forget.ts)
- [src/functions/access-tracker.ts](src/functions/access-tracker.ts)
- [src/types.ts](src/types.ts)
- [src/state/schema.ts](src/state/schema.ts)
</details>

# Memory Lifecycle — Write, Age, Decay, Forget

This page describes the full end-to-end journey of a memory in agentmemory: from its initial write through `mem::remember`, through the scoring pipeline that assigns it a hot/warm/cold retention tier, to its eventual eviction or crystallization into a durable long-term form. Understanding this lifecycle lets you predict which memories survive across sessions, why some are silently pruned, and how the system converts raw episodic records into compressed semantic or procedural knowledge.

The lifecycle is governed by two orthogonal timers: a wall-clock TTL set at write time (`forgetAfter`) and a continuously decaying retention score computed from salience, age, and access frequency. Either one can end a memory independently, but the consolidation pipeline can also rescue content by abstracting it into higher-fidelity forms — `SemanticMemory`, `ProceduralMemory`, and `Crystal` — that survive long after the original episodic record would have been pruned.

---

## 1. Birth — `mem::remember`

A memory is created by calling `mem::remember` (registered in `src/functions/remember.ts`). The function accepts `content`, an optional `type`, `concepts`, `files`, a `ttlDays` duration, and `sourceObservationIds`.

### Deduplication before creation

Before writing, `mem::remember` computes Jaccard similarity between the incoming content and every existing memory whose `isLatest === true`. If any existing memory exceeds the 0.7 similarity threshold, the new memory is treated as a supersession of that prior entry:

```ts
// src/functions/remember.ts:58-69
for (const existing of existingMemories) {
  if (existing.isLatest === false) continue;
  const similarity = jaccardSimilarity(
    lowerContent,
    existing.content.toLowerCase(),
  );
  if (similarity > 0.7) {
    supersededId = existing.id;
    ...
    break;
  }
}
```

The superseded memory has `isLatest` set to `false` and is kept in the KV store (as a historical version), while the new record carries `version: supersededVersion + 1`, `parentId`, and `supersedes: [supersededId]`.

### Initial field values

The newly minted `Memory` object starts with `strength: 7` and the following type-weight mapping governs its salience at scoring time:

| `type`        | Base salience |
|---------------|--------------|
| `architecture`| 0.9          |
| `preference`  | 0.85         |
| `pattern`     | 0.8          |
| `bug`         | 0.7          |
| `workflow`    | 0.6          |
| `fact`        | 0.5          |

Sources: [src/functions/remember.ts:38-48](), [src/functions/retention.ts:100-119]()

### TTL stamp

If `ttlDays` is provided, a `forgetAfter` ISO timestamp is computed immediately:

```ts
// src/functions/remember.ts:92-94
memory.forgetAfter = new Date(Date.now() + data.ttlDays * 86400000).toISOString();
```

This timestamp is checked by both `mem::evict` and `mem::auto-forget` on every maintenance sweep.

### Index registration

After the KV write, the memory is synchronously registered with the BM25 full-text index (`getSearchIndex().add(...)`) and asynchronously registered with the vector index (`vectorIndexAddGuarded`). An indexing failure is caught and logged but does not roll back the KV write — a restart-time rebuild will pick up the memory either way.

Sources: [src/functions/remember.ts:100-121]()

---

## 2. Access Tracking

Every time a memory is retrieved, `recordAccess` (in `src/functions/access-tracker.ts`) is called to update the memory's `AccessLog`:

```ts
export interface AccessLog {
  memoryId: string;
  count: number;      // total lifetime accesses
  lastAt: string;     // ISO timestamp of most recent access
  recent: number[];   // up to 20 recent Unix-ms timestamps (RECENT_CAP = 20)
}
```

Access records are written under the `mem:access` KV namespace, keyed by `memoryId`. The write is guarded by a per-memory keyed mutex (`mem:access:<memoryId>`) to prevent concurrent update races. The `recent` array is capped at 20 entries — older entries are dropped from the front — preserving a sliding window of recent access timestamps for use in the reinforcement boost calculation.

Sources: [src/functions/access-tracker.ts:6-80]()

---

## 3. Retention Scoring — Hot, Warm, Cold, Evictable

`mem::retention-score` (in `src/functions/retention.ts`) computes a `[0, 1]` score for every `isLatest` episodic memory and every semantic memory. This is the central mechanism that determines a memory's tier.

### The decay formula

```
score = min(1, salience × exp(−λ × ΔT_days) + boost)
```

where:

- **salience** = base type weight + min(0.2, accessCount × 0.02)
- **ΔT_days** = age in days since `createdAt`
- **λ (lambda)** = decay rate, default `0.01` (slow decay — a memory halves in ~69 days with no accesses)
- **boost** = Σ(σ / daysSinceAccess) across recent access timestamps, where **σ (sigma)** = `0.3`

The reinforcement boost is large for recent accesses and drops off as access recency fades. Each access event injects `σ / daysSinceAccess` into the score — a memory accessed yesterday contributes `0.3`, one accessed 30 days ago contributes `0.01`.

### Default tier thresholds

| Tier        | Score range         |
|-------------|---------------------|
| **hot**     | ≥ 0.7               |
| **warm**    | ≥ 0.4 and < 0.7     |
| **cold**    | ≥ 0.15 and < 0.4    |
| **evictable**| < 0.15             |

These thresholds are configurable via a `DecayConfig` object passed to the function. The defaults live in `retention.ts`:

```ts
// src/functions/retention.ts:19-27
const DEFAULT_DECAY: DecayConfig = {
  lambda: 0.01,
  sigma: 0.3,
  tierThresholds: { hot: 0.7, warm: 0.4, cold: 0.15 },
};
```

### Batched writes

After scoring all memories, the results are flushed to the `mem:retention` KV namespace in a single parallel `Promise.all` batch to avoid O(n) sequential round-trips on stores with 1000+ memories.

Sources: [src/functions/retention.ts:80-288](), [src/types.ts:853-876]()

### State diagram of a memory's retention tier

```stateDiagram-v2
    [*] --> hot : created (strength=7, young)
    hot --> warm : age grows, fewer accesses
    warm --> cold : further decay
    cold --> evictable : score < 0.15
    evictable --> [*] : mem::retention-evict
    warm --> hot : frequent accesses (boost)
    cold --> warm : re-accessed
    hot --> [*] : forgetAfter TTL reached
    warm --> [*] : forgetAfter TTL reached
    cold --> [*] : forgetAfter TTL reached
```

---

## 4. Eviction Paths

There are two complementary eviction functions. They differ in scope and trigger.

### 4a. `mem::retention-evict` — score-based pruning

Reads all rows from `mem:retention`, filters those whose `score < threshold` (default: `cold` boundary, 0.15), sorts by ascending score (lowest first), and deletes up to `maxEvict` (default 50, capped at 1000). For each candidate, it:

1. Resolves the correct KV namespace (`mem:memories` for episodic, `mem:semantic` for semantic)
2. Deletes the memory record
3. Deletes the retention score row
4. Deletes the access log row via `deleteAccessLog`

Sources: [src/functions/retention.ts:291-407]()

### 4b. `mem::evict` — structural eviction

`mem::evict` (in `src/functions/evict.ts`) handles four distinct eviction scenarios on each sweep:

| Scenario | Condition | Action |
|---|---|---|
| **Stale session** | Session age > `staleSessionDays` (default 30d) and no summary | Attempt `event::session::stopped` recovery; then delete session |
| **Low-importance observations** | Observation age > `lowImportanceMaxDays` (default 90d) and `importance < 3` | Delete observation |
| **Observation cap** | Project exceeds `maxObservationsPerProject` (default 10,000) | Evict lowest-importance observations |
| **Expired memories (TTL)** | `mem.forgetAfter` is past | Delete memory + access log |
| **Old non-latest versions** | `isLatest === false` and age > 90d | Delete superseded version |

All evictions emit an audit record in `mem:audit`.

Sources: [src/functions/evict.ts:15-346]()

### 4c. `mem::auto-forget` — autonomous cleanup

`mem::auto-forget` (in `src/functions/auto-forget.ts`) runs three sub-passes:

1. **TTL pass**: Deletes any memory whose `forgetAfter` has elapsed (identical to `mem::evict`'s TTL pass).
2. **Contradiction pass**: Compares all `isLatest` memories that share concepts using token-level Jaccard similarity. If similarity exceeds 0.9, the older memory has `isLatest` set to `false` (soft-delete, not hard-delete). This avoids redundant or conflicting facts accumulating.
3. **Low-value observation pass**: Deletes observations older than 180 days with `importance ≤ 2`.

Sources: [src/functions/auto-forget.ts:39-186]()

---

## 5. The Consolidation Pipeline

The consolidation pipeline (`mem::consolidate-pipeline` in `src/functions/consolidation-pipeline.ts`) transforms raw episodic content into three long-lived derived types. It is guarded by the `CONSOLIDATION_ENABLED` config flag and runs as a triggered function (usually post-session or on a schedule).

The pipeline supports four named tiers, all run when `tier === "all"`:

### Tier: `semantic`

Requires at least 5 session summaries. Takes the 20 most recent `SessionSummary` records, prompts the provider with `SEMANTIC_MERGE_SYSTEM`, and parses `<fact confidence="0.9">...</fact>` XML tags from the response. Each extracted fact either updates an existing `SemanticMemory` (incrementing `accessCount`, raising `confidence`) or creates a new one with `strength = confidence`.

```ts
// src/functions/consolidation-pipeline.ts:86-118
while ((match = factRegex.exec(response)) !== null) {
  const fact = match[2].trim();
  const existing = existingSemantic.find(s => s.fact.toLowerCase() === fact.toLowerCase());
  if (existing) {
    existing.accessCount++;
    existing.confidence = Math.max(existing.confidence, confidence);
    await kv.set(KV.semantic, existing.id, existing);
  } else {
    const sem: SemanticMemory = { id: generateId("sem"), fact, confidence, strength: confidence, ... };
    await kv.set(KV.semantic, sem.id, sem);
  }
}
```

### Tier: `reflect`

Delegates to `mem::reflect`, which clusters recent memories and surfaces cross-session patterns.

### Tier: `procedural`

Requires at least 2 recurring `pattern`-type memories (those with `sessionIds.length >= 2`). Prompts the provider to extract `<procedure name="..." trigger="..."><step>...</step></procedure>` blocks. New procedures are stored in `mem:procedural`; existing ones have `frequency` incremented and `strength` raised by `+0.1`.

### Tier: `decay`

Applies strength decay to all semantic and procedural memories using `applyDecay`, which applies `strength × 0.9^n` per full decay period (default decay interval: `getConsolidationDecayDays()`):

```ts
// src/functions/consolidation-pipeline.ts:21-43
function applyDecay(items, decayDays) {
  for (const item of items) {
    const daysSince = (now - new Date(item.lastAccessedAt || item.updatedAt).getTime()) / 86400000;
    if (daysSince > decayDays) {
      const periods = Math.floor(daysSince / decayDays);
      item.strength = Math.max(0.1, item.strength * Math.pow(0.9, periods));
    }
  }
}
```

Note: this decay operates on `strength` (a field on the record) rather than the separate `RetentionScore`. The minimum strength is clamped to `0.1` — semantic and procedural memories are never fully zeroed out by time alone; they require eviction via `mem::retention-evict` if their retention score falls below the threshold.

Sources: [src/functions/consolidation-pipeline.ts:1-270]()

---

## 6. Crystallization — Locking Completed Work Into Long-Term Patterns

Crystallization converts a completed chain of `Action` records into a compact `Crystal` that captures what was accomplished, key outcomes, affected files, and extracted lessons.

`mem::crystallize` (in `src/functions/crystallize.ts`) requires all referenced actions to have `status === "done"` or `"cancelled"`. It sends the action chain to the provider using `CRYSTALLIZE_SYSTEM` prompt and parses a JSON response into a `CrystalDigest`. The resulting `Crystal` is written to `mem:crystals`. Each lesson extracted from the digest is separately stored via `mem::lesson-save` at confidence `0.6`.

```ts
// src/functions/crystallize.ts:60-92
const crystal: Crystal = {
  id: generateId("crys"),
  narrative: digest.narrative,
  keyOutcomes: digest.keyOutcomes,
  filesAffected: digest.filesAffected,
  lessons: digest.lessons,
  sourceActionIds: data.actionIds,
  ...
};
await kv.set(KV.crystals, crystal.id, crystal);
// Propagate lessons
await Promise.all(digest.lessons.map(lesson => sdk.trigger({ function_id: "mem::lesson-save", ... })));
// Mark source actions as crystallized
for (const action of actions) {
  await kv.set(KV.actions, action.id, { ...action, crystallizedInto: crystal.id });
}
```

`mem::auto-crystallize` scans all `done` actions older than `olderThanDays` (default 7) that have no `crystallizedInto` field, groups them by `parentId ?? project ?? "_ungrouped"`, and crystallizes each group in sequence.

A crystal is a terminal form — it has no retention score and is not subject to the normal decay pipeline. Its extracted `Lesson` records carry their own `decayRate` field and can be independently reinforced or decayed over time.

Sources: [src/functions/crystallize.ts:18-229](), [src/types.ts:727-754]()

---

## 7. Full Lifecycle Overview

```text
┌─────────────────────────────────────────────────────────────────┐
│  WRITE                                                          │
│  mem::remember → Memory{isLatest=true, strength=7, ttl?}        │
│    ↓ Jaccard dedup → supersedes older version (isLatest=false)  │
│    ↓ BM25 + vector index registration                           │
└──────────────────────────┬──────────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────────┐
│  ACCESS TRACKING                                                │
│  recordAccess → AccessLog{count, lastAt, recent[20]}            │
│    drives reinforcement boost in retention scoring              │
└──────────────────────────┬──────────────────────────────────────┘
                           │
┌──────────────────────────▼──────────────────────────────────────┐
│  RETENTION SCORING  (mem::retention-score)                      │
│  score = min(1, salience × e^(−λ·ΔT) + Σ(σ/daysSinceAccess))  │
│  Tiers:  hot ≥0.7 | warm ≥0.4 | cold ≥0.15 | evictable <0.15  │
└──────────┬────────────────────────────┬───────────────────────-─┘
           │ score < 0.15               │ score survives
           ▼                            ▼
┌──────────────────┐        ┌──────────────────────────────────┐
│  EVICTION        │        │  CONSOLIDATION PIPELINE          │
│  retention-evict │        │  semantic: facts → SemanticMemory │
│  evict (TTL,     │        │  procedural: patterns → Procedure │
│   stale session, │        │  reflect: cluster → insights      │
│   cap, non-latest│        │  decay: strength × 0.9^n         │
│  auto-forget     │        └──────────────┬───────────────────┘
│   (TTL, contra-  │                       │
│    diction, low- │                       ▼
│    value obs)    │        ┌──────────────────────────────────┐
└──────────────────┘        │  CRYSTALLIZATION                 │
                            │  completed actions → Crystal     │
                            │  + Lessons (confidence 0.6)      │
                            └──────────────────────────────────┘
```

---

## 8. Configuration Reference

| Parameter | Default | Where set | Effect |
|---|---|---|---|
| `lambda` | `0.01` | `DecayConfig` | Exponential decay rate (per day) |
| `sigma` | `0.3` | `DecayConfig` | Reinforcement boost multiplier |
| `hot` threshold | `0.7` | `DecayConfig.tierThresholds` | Minimum score for hot tier |
| `warm` threshold | `0.4` | `DecayConfig.tierThresholds` | Minimum score for warm tier |
| `cold` threshold | `0.15` | `DecayConfig.tierThresholds` | Eviction boundary |
| `staleSessionDays` | `30` | `EvictionConfig` / `mem:config/eviction` | Session age before eviction |
| `lowImportanceMaxDays` | `90` | `EvictionConfig` | Max age for low-importance observations |
| `lowImportanceThreshold` | `3` | `EvictionConfig` | Importance cutoff for old observations |
| `maxObservationsPerProject` | `10,000` | `EvictionConfig` | Per-project observation cap |
| `CONSOLIDATION_ENABLED` | (unset) | env var | Gates the full consolidation pipeline |
| `olderThanDays` | `7` | `mem::auto-crystallize` | Age before auto-crystallization |
| Contradiction threshold | `0.9` | `auto-forget.ts` constant | Jaccard similarity for near-duplicate detection |

---

## 9. Failure Modes

| Failure | Behavior |
|---|---|
| BM25 index write fails on `mem::remember` | Logged as warning; KV write already succeeded; memory is invisible to BM25 search until restart-time rebuild |
| `mem::retention-score` not run before `mem::retention-evict` | Evict has no scores to work with; `allScores` returns empty; nothing is evicted |
| Pre-0.8.10 retention row has no `source` field | Evict probes both `KV.memories` and `KV.semantic` to find the record; adds one extra KV round-trip per candidate |
| Stale session recovery (`event::session::stopped`) fails | Session is skipped; no eviction; logged as warning |
| Consolidation pipeline disabled (`CONSOLIDATION_ENABLED` unset) | All tiers return `{ skipped: true }` immediately |
| `mem::crystallize` called on non-`done` action | Returns error immediately; no partial write |

Sources: [src/functions/remember.ts:106-114](), [src/functions/retention.ts:326-365](), [src/functions/evict.ts:56-80]()

---

The memory lifecycle in agentmemory is thus a continuous balance: every access event injects a reinforcement boost that slows decay; the absence of access lets the exponential curve push a memory toward the evictable tier; and the consolidation pipeline provides an escape hatch by which frequently-recurring patterns survive indefinitely as semantic facts, procedural workflows, or crystallized action digests regardless of their episodic retention score. Sources: [src/functions/retention.ts:80-94](), [src/functions/consolidation-pipeline.ts:21-43](), [src/functions/crystallize.ts:60-92]()
