Memory tiers
Engram models memory the way agents actually use it: most facts are only relevant for minutes or hours, a few deserve to persist. Two tiers encode that:
- STM (short-term) — cheap to write, expires on its own, no embedding required. The default landing zone for observations.
- LTM (long-term) — durable, embedded, semantically searchable, subject to lifecycle jobs (decay, dedup, consolidation).
The split is behavioral, not structural — both tiers share the
same Memory model. What differs is
where rows live, how long they live, and which jobs touch them.
STM: expiry as a feature
Section titled “STM: expiry as a feature”PostgresStmAdapter (packages/memory-stm) stores short-term memories as
rows in the shared memories table — type: 'short-term' with an
expiresAt timestamp — injected through the STM_PROVIDER token.
Every STM write carries a TTL — default 86 400 s (24 h), clamped to
[60 s, 7 d] — recorded as expiresAt. Nothing an agent does has to
garbage-collect stale session facts; they evaporate. This is why transient
state belongs in STM: forgetting is free.
Two details matter for the rest of the system:
- Access counting. Every STM read increments the memory’s
accessCount. That counter is the promotion signal: a “transient” fact an agent keeps re-reading is evidently not transient. See Consolidation & decay for the promotion pass (STM_CONSOLIDATION_ACCESS_THRESHOLD, default 3). - One table, swept clean. STM rows live in the same
memoriestable as LTM (one model, two lifecycles), so promotion is a field update, not a cross-store copy. Expiry is the only STM-specific mechanic: reads exclude rows pastexpiresAt, andStmSweepService(STM_SWEEP_INTERVAL_MS) deletes them in the background.
LTM: durability plus lifecycle
Section titled “LTM: durability plus lifecycle”MemoryLtmService (packages/memory-ltm) persists long-term memories to
Postgres — the system’s source of truth — and runs the write-time ingest
pipeline: exact dedup, semantic dedup, contradiction detection, and
importance scoring happen before the row lands, and embedding/indexing
follow asynchronously. LTM rows are the only rows that:
- get embeddings and participate in semantic recall,
- are subject to decay, pruning, and corpus consolidation,
- carry the audit trail and version CAS guarantees.
Both profiles persist LTM to the same Postgres store; the profile only changes whether the multi-tenant auth stack sits in front of it.
Crossing the boundary
Section titled “Crossing the boundary”There are three ways a memory moves or lands between tiers:
- Explicit —
create_memorywithtype: 'long-term', orpromote_memoryon an existing STM row. - Heuristic —
rememberwithtype: 'auto'(the default posture for agents) routes to a tier from content signals and TTL hints. - Automatic — the consolidation pass promotes STM rows whose
accessCountand importance clear the thresholds.
Promotion preserves the memory’s id and content; the row simply becomes
type: 'long-term', loses expiresAt, and enters the LTM lifecycle
(embedding, recall, decay).
Why not one tier?
Section titled “Why not one tier?”A single durable tier forces a bad choice on every write: either everything persists (and recall drowns in expired session noise, while the corpus grows unboundedly) or the agent must decide durability perfectly at write time (it cannot). The two-tier design lets writes default to cheap-and-forgettable, and uses observed access behavior — not write-time guesses — to earn durability. Decay then applies the same principle in reverse to LTM: memories that stop being accessed lose importance and are eventually marked stale or pruned. Between promotion and decay, the corpus tracks what is actually useful.