Pattern · Lifecycle

Promotion Between Tiers

Storage tiers are common, and so is a computable rule for moving a memory up one. Keeping the rule's inputs apart, capping it by provenance and recording why a memory moved are much rarer — and without them nobody can say what the long-term store is made of.

Intent

If memory has storage tiers, name the rule that promotes between them, make it computable, and be able to say why any given record is in the tier it is in.

The page makes two kinds of claim. A computable rule for moving a memory between storage tiers — working to episodic, short- to mid- to long-term, a live store to a durable file — is reported practice: MemoryOS, MemOS, dsh-ai-memory and cortexgraph each ship one. That the rule's inputs stay separate, that provenance caps it, and that each promotion is recorded with its reasons is argued, from partial instances named below.

The line with trust-state machine runs between where a memory is stored and what is believed about it: a status that decides whether a memory may be acted on — a corroboration threshold, an outcome-counted ladder, a provenance cap on a verified label — belongs there, and this page is about the move from one store to another.

The problem

Tiering is a common structure in agent memory: short-term and long-term, hot and archival, working and durable, core and recall. It is almost free to describe, which is why so many systems have it.

What varies is the answer to what moves a memory up. The usual shapes:

  • No rule at all. A summarizer runs every N turns and whatever it emits is now long-term. The tier boundary is a schedule, not a judgement.
  • A rule inside a prompt. An LLM is asked to decide what is worth keeping, with the criteria in prose that nobody reviews as policy.
  • A rule nobody validated. A formula with named coefficients, shipped at their defaults, with no ablation.

All three produce the same failure, and it is quiet: the long-term store fills with whatever the promotion path happened to favour, and because the path is unstated nobody can say what that was. Memory does not break; it slowly stops being about the right things, and the only symptom is answers getting less useful in ways nobody can attribute.

A second failure follows from the first. If promotion is unexplained, so is its inverse — a record that never promotes is invisible, and not promoting is the decision with no error message.

The pattern

tier boundary
   │
   ├─ a written rule, computable from stored fields
   ├─ inputs that are separately meaningful, not one blended score
   ├─ a ceiling: what a record's provenance forbids, regardless of use
   └─ a record of why this one promoted, queryable later
Diagram — promotion runs on a written rule over separable stored inputs, capped by provenance and recorded with its reasons; blending the inputs into one score is what makes a promotion unattributable
Diagram source
%% caption: promotion runs on a written rule over separable stored inputs, capped by provenance and recorded with its reasons; blending the inputs into one score is what makes a promotion unattributable
flowchart TD
    Rec["record in the lower tier"] --> Rule["written rule, computed from stored fields"]
    In["separable inputs:<br/>frequency · recency · importance · corroboration"] --> Rule
    Rule --> Cap{"does provenance justify the climb?"}
    Cap -- "no" --> Stay["stays — capped by its source,<br/>however often it is used"]
    Cap -- "yes" --> Up["promoted to the upper tier"]
    Up --> Why["decision recorded: inputs, rule, actor"]
    In -. "blended into one score" .-> Opaque["cannot attribute<br/>the promotion"]

Four requirements:

  1. Write the rule down, as something a program computes rather than a sentence a model interprets. A formula with named coefficients can be ablated; a prompt cannot.
  2. Keep the inputs separable. Frequency, recency, importance and corroboration answer different questions, and a single blended scalar cannot say which one promoted a record.
  3. Give provenance a veto. Use should be able to raise a record's standing and never past what its source justifies — otherwise a guess that got popular becomes canon.
  4. Record the decision, so "why is this in long-term memory" is a lookup.

Why it works

It converts a boundary that everything crosses into a decision that can be examined. Once the rule is computable you can ablate it, once the inputs are separable you can attribute a promotion, and once provenance caps the ladder the worst failure — a popular error becoming permanent — is structurally unavailable rather than merely unlikely.

Tradeoffs

  • A written rule invites false confidence. A formula looks principled at a glance; unvalidated coefficients are a prompt with extra steps.
  • Separable inputs cost schema. Four fields where one would do, each needing a definition and a writer.
  • A ceiling can be wrong. If provenance is asserted rather than verified, the cap inherits the honesty of whatever set it.
  • Promotion is one-way in most implementations. Demotion is rarer and harder, and a monotonic ladder means an early mistake is permanent unless correction lives on a different axis.

Cost to adopt

Build: the fields the rule reads, the rule itself, and a place to record why a promotion happened.

Forces elsewhere: retrieval must respect tiers or they are decoration, and the background pass that promotes needs the same recoverability as any other — a promotion pass that dies halfway leaves the boundary in an undefined state.

Ongoing: the coefficients are a tuning surface with no natural feedback. If nothing measures promotion quality, the rule is frozen at whatever its author guessed.

Skip it if you have one tier. Two tiers with no rule is worse than one tier, because it implies a judgement that is not being made.

Seen in the atlas

The computable rule — reported

MemoryOS writes the rule down, which is the right instinct and shows the trap: alpha·N_visit + beta·L_interaction + gamma·R_recency with all three coefficients left at 1 and no ablation. Because the terms are summed, L_interaction means a long conversation scores like a frequently-revisited one — verbosity is indistinguishable from importance, and long-term memory is built from what wins. It also keeps a second access counter for LFU eviction, so the number that decides what is hot and the number that decides what is deleted can disagree about the same segment.

MemOS has the same shape one step further out. On a timed trigger its scheduler sorts working memories by an importance score weighting list position, a keyword score and a recording count [0.9, 0.05, 0.05], and copies the top slice into activation memory as KV-cache items — one blended scalar, so which input moved a memory is not recoverable.

cortexgraph promotes out of its live store into a Markdown vault a person reads: a memory above a strength threshold and with enough use is written out, and promoted_at and promoted_to are set on the row.

dsh-ai-memory shows both ways a tier rule fails without any model involved. Working notes move to episodic after an age, and episodic notes to an untimed profile tier after an age once access_count reaches a minimum of two. Its chat preset expires a working note at the same age it would promote it, and consolidate checks the TTL first, so under the preset the plugin ships an unpinned working note is deleted rather than promoted — pinned notes skip expiry and can promote — while the type's own doc comment says to set retention longer than the promote delay. And access_count is incremented for every hit the per-prompt prefetch returns, 128 of them with no floor, so being injected twice is being accessed twice and the profile tier fills with whatever lasted a week.

dsh-layered-memory promotes L1 records into L2 scene files on a count, and hands the next tier the wrong set. The per-family counter newMemoriesSinceL2 accumulates across extractions, but when it reaches five the scene pass receives only the records of the extraction that crossed it, and the counter resets to zero. Records from every turn that stayed under the threshold never reach a scene, nor the persona built from scenes, unless a rebuild runs; the rebuild's own comment names the gap. A count that decides promotion has to travel with the items it counted.

Other systems tier with no rule over stored fields at all. Letta's core, archival and recall tiers are written by the agent's own tool calls, so the rule is whatever the model decides — the rule-inside-a-prompt shape. LoongFlow's stm, mtm and ltm pass material through a Compressor. Mercury is not one of them: its heartbeat moves active and durable rows unseen for 30 days into a subconscious tier ordinary recall does not search, and recall promotes a row back when fewer than three conscious matches clear a health threshold — but last_seen_at moves on a merge or a promotion, not on retrieval, so a memory in daily use still demotes. Redis Agent Memory Server works the other end of the lifecycle: its forgetting policy combines TTL, inactivity, pinning, per-type allowlists and budget pruning, and what it selects is deleted rather than moved to a lower tier.

sqlite-memory-mcp builds a governed promotion from candidate claim to canonical fact and then opens two doors beside it. The governed door, promote_candidate, takes its mode from the caller and defaults to human_confirmed, so the agent that extracted a claim can promote it by omitting an argument. The ungoverned door runs on every observation write: a regex claim gains 0.1 confidence per matching observation and is inserted into canonical_facts as auto_lazy at 0.85, three or four sentences in, with no gate consulted. Both land in one table and render identically as FACT in the context pack, so the tier boundary records who promoted a fact without changing how it is read.

eMEM computes its tier rule from the wrong clock. Its archival sweep selects long_term observations whose event timestamp is older than archive_after_seconds, while the setting is commented as time spent in long_term, and no column records when a row was promoted — so all but the last hour of a long episode is archived on the first sweep after consolidation. The tier DBSCAN rejects has no exit at all: noise points stay short_term, which archival never selects, and are re-clustered on every time-window pass for as long as the file exists.

Roampal Core computes its tier rule from ratings the consuming model gives. Summaries enter working at 0.5 and move to history at 0.7 with two uses, and to patterns at 0.9 with three uses and five successes counted since promotion; unknown, meaning the memory was shown and not used, subtracts 0.05, so retrieval noise drifts a pattern toward demotion. A working row not promoted within 24 hours is deleted, and demotion keeps the original created_at, so a demoted pattern past 30 days is deleted at the next history cleanup. The companion paper's headline accuracy was measured with promotion and decay disabled, so its own benchmark does not measure the rule.

archeus promotes a lesson from one project's graph into the user-level CLAUDE.md by recurrence: a reviewed lesson needs two projects and a pending one needs three, with the comment that "waiting for a manual approval that never comes is why this feature sat empty" (claude_sessions/conventions.py:20-26). The rule is computable and the threshold is provenance-aware, which is the shape this pattern asks for. It also shows the cost: the promoted tier is read by every session on the account, so a lesson nobody approved reaches the widest scope the system has, while the per-project tier it came from still withholds it.

Separate inputs, a ceiling, a recorded decision — argued

No system cited here has all three on a storage-tier move; each has part.

Separate inputs. NOOA Memory keeps importance, salience, confidence and a spaced-repetition strength apart, and retrieval moves only the last. Its paper adds the detail that closes the loop: "injected memories are not reinforced, so what the harness surfaces does not distort the usage signal" — the fix for dsh-ai-memory's prefetch.

A ceiling. Core Memory caps a ladder by its source. Grounding — observed, extracted, inferred, speculative — bounds the C/B/A confidence_class a bead can reach: a speculative bead "cannot reach canonical status", explicitly "not via recall, not even via promotion", and committed cases assert it across an index rebuild. The ladder it caps ranks and sets assembly depth rather than moving a bead between stores, so this is the ceiling shown on a confidence ladder; a status cap of the same kind is on trust-state machine.

A recorded decision. CSM promotes from a candidate queue into durable memory through five gates — confidence, reinforcement, zero contradictions, evidence references and distinct source sessions — and every decision carries a thresholdChecks object with actual against required for each, so a promotion report explains its own refusals. It ships disabled and dry-run by default, and minSessions defaults to 1, which makes the session-diversity gate a no-op until an operator raises it. OmniIntelligence records the same thing on a status ladder: every transition writes a row with a gate_snapshot of the gate conditions at transition time, under a foreign key that refuses deletion.

Cambium names the inputs that must not qualify. Its status standard says "A status MUST NOT be upgraded directly because the file exists, its length reaches a threshold, or automated checks pass", and its deterministic scripts emit only fail and candidate, so the tooling can nominate work and has no outcome that promotes. It governs status axes on wiki pages rather than storage tiers, and nothing in the repository enforces the prohibition; the list of cheap signals — an age, a size, a passing check — applies to a storage-tier rule unchanged.

The measured warning comes from NOOA Memory's paper, and it applies to whatever promotion produces: reflection records are "22% of rows yet ~1% of both read channels". A fifth of that store is distilled material that retrieval almost never surfaces. Promotion succeeded; usefulness did not follow.

Mind ships the simplest computable rule on this page and records none of its reasons. Tier 1 holds 25 and tier 2 holds 50 non-pinned memories per space; every non-pinned memory_read promotes the memory one tier, and a write or promotion into a full tier demotes the unpinned row with the oldest COALESCE(last_accessed_at, created_at) (src/store/repositories/memory-repository.ts:126-169, :571-590). Nothing is ever evicted to deletion, and tier never enters the search score. The one input is a read, which also serves inspection, so the protocol has to tell the model to pass noPromote: true when it only looks; no row records why a memory moved, and access promotion leaves changed_at untouched.

Tests to require

  • State the rule, then compute it by hand for ten promoted records and ten that were not. If you cannot, it is not a rule.
  • Ablate each term. If removing one does not change which records promote, it is not doing work.
  • Feed the system a long, low-value exchange and a short, decisive one, and check which promotes.
  • Assert a low-provenance record cannot reach the top tier by any amount of retrieval.
  • Measure what fraction of promoted records are ever read back. If the top tier is not being retrieved, promotion is a cost with no return.
  • Kill the promotion pass halfway and assert the tier boundary is still well-defined.
  • Check that retrieval actually respects the tiers, rather than ranking across all of them and rendering the boundary decorative.