SmartMemory
Concepts

Memory Dynamics

πŸ“š Repository: smartmemory-ai/smart-memory πŸ› Issues: Report bugs or request features

SmartMemory doesn't treat every stored item as equally important forever. Memories gain and lose prominence over time based on how you use them, and the retrieval layer uses those signals to surface what's actually relevant to a query β€” not just what lexically matches.

This page explains the memory-dynamics model: how activation, decay, compression, spreading, and centrality combine to decide what comes back from a get_working_context() call.

Why this matters

A naΓ―ve memory system treats retrieval as lexical search: you ask "why did we pick JWT?" and you get back items that contain the words "why," "pick," or "JWT." Everything you know that's related to JWT but doesn't contain the word β€” load-balancer constraints, session-cookie failure modes, the decision thread that referenced a specific RFC β€” stays invisible unless your query happens to match its text.

SmartMemory's dynamics model closes that gap. Frequently-used items get promoted; unused items fade; analytical questions pull in graph neighbors even when they don't lexically match; items you explicitly pin for a session are guaranteed on top. The result is that get_working_context() returns contextually relevant items, not just lexically matching items.

The four behaviors

Four things happen in the background and at retrieval time.

1. Memory you use stays warm

Every time you retrieve an item via search(), get(), or a spreading-activation pass, its activation score goes up. The boost is a small additive nudge (0.05 by default for a direct retrieval, smaller for surfacing and co-retrieval). Over a session of active use, frequently-touched items accumulate significantly higher activation than passive ones.

# Behind the scenes: every retrieval queues a boost to Redis.
# A background worker (ActivationFlushJob) drains the queue every 5s
# and writes the updated score to the item's metadata.
memory.search("authentication")  # items in the results get a +0.05 boost
memory.get("item-123")           # item-123 gets a +0.05 boost

You don't call a boost API directly. The system just tracks which memories actively participate in your workflow.

2. Unused memory fades (and eventually compresses)

Activation has a half-life (default: 30 days). A memory you haven't touched in a month is at roughly half the activation of when you last used it. After ~90 days of no activity, its activation approaches zero.

Fading is visible two ways:

  • Ranking β€” low-activation items rank lower in retrieval results (they have to be compensated for by strong lexical relevance or explicit pinning).

  • Compression β€” very cold items get progressively compressed through a five-tier ladder:

    FULL β†’ SUMMARY β†’ SKELETON β†’ TOMBSTONE β†’ FORGOTTEN

    At the SUMMARY tier, the item's content is replaced with an LLM-generated digest. At SKELETON, just a title. TOMBSTONE keeps only the id. FORGOTTEN is removable by workspace policy. User-authored content (origin tier 1 β€” anything added via memory.add(), CLI, or API) is never compressed past FULL regardless of activation.

Compression is opt-in (SmartMemory(tier_compaction=True) + SMARTMEMORY_COMPACTION_ENABLED=true env var). Decay is always on.

When you ask a "why" or "how" question, SmartMemory's surfacing router classifies it as deep:multi-hop and does more than lexical search:

  1. Find the top lexical hits (the items that directly match your query's words).
  2. Spread from those seeds along the graph β€” bounded BFS, default 2 hops deep, 20-neighbor fan-out. Items reachable through RELATED, MENTIONS, ANSWERED_BY, PART_OF, and similar edges contribute to the candidate set even if they don't lexically match.
  3. Compute Personalized PageRank from the same seeds over the workspace's subgraph. Items with high graph centrality relative to the seeds get a ranking lift.
  4. Combine lexical relevance, activation, recency, spreading score, and PPR centrality into a single composite score.

The practical effect: ask "Why did we pick JWT?" and get back the JWT decision item, plus the load-balancer note, plus the session-cookie failure thread, plus the RFC reference β€” not because they all contain "JWT," but because they sit in the graph neighborhood of the items that do.

Two other strategies the router can pick:

  • fast:recency β€” short conversational queries. Skips spreading and PPR entirely; ranks by recent activity and lexical match. Sub-50ms p50.
  • factual:point-query β€” short lookups like "users" or "list of decisions". Graph-only, skips vector semantic search (which would drown a single-entity query in noise).

Three more strategies (procedural:lookup, temporal:windowed) are reserved in the enum for a future release and currently soft-degrade to fast:recency.

4. You can pin items for a session

For conversational workflows, you often want to say "keep this on top for the rest of our chat." SmartMemory supports session-scoped pins:

memory.pin_item(session_id="my-session", item_id="project-requirements-doc")

# Now every get_working_context(session_id="my-session", ...) call
# force-includes project-requirements-doc in the results.
memory.unpin_item(session_id="my-session", item_id="project-requirements-doc")

Pins are Redis-backed, TTL'd at 24 hours, and workspace-bound β€” the first pin binds the session to one workspace, and subsequent cross-workspace pins raise SessionPinWorkspaceConflict. Reads of another workspace's pin set via a reused session id return empty. This prevents a poisoned session from bridging memory across tenants.

The composite score

Every item in a get_working_context() response comes with a score breakdown showing exactly what contributed to its rank:

resp = memory.get_working_context(session_id="s1", query="Why did we pick JWT?")

resp["items"][0]["score_breakdown"]
# {
#   "activation":         0.73,   # how warm the item is
#   "relevance":          0.92,   # lexical match to query
#   "recency":            0.88,   # exponential decay on last modified
#   "centrality":         0.65,   # normalized PPR score over candidates
#   "anchor_forced":      false,  # true = force-included anchor
#   "session_pin_boost":  0.0,    # pin_weight Γ— 0.3 for pinned items
#   "freshness_boost":    0.15,   # linear decay over 24h for new items
# }

The composite formula (surfacing_mode="router"):

base            = max(activation, 0.05) Γ— relevance Γ— recency
centrality_lift = 1 + 0.5 Γ— centrality_norm       # 1.0 .. 1.5
additives       = anchor_forced + session_pin_boost + freshness_boost

score = base Γ— centrality_lift + additives

Additives like pins and anchors don't multiply the score β€” they add a fixed lift and force the item into the returned set regardless of its base rank. A freshly-pinned item with zero activation still surfaces.

Opt-in and defaults

All of this is off by default. Every existing caller of get_working_context() gets the M1a legacy behavior (lexical Γ— recency Γ— activation-with-floor) until they explicitly opt in:

memory = SmartMemory(surfacing_mode="router")  # turns on router + composite + pins

With surfacing_mode="router" off:

  • The router is bypassed; all queries use fast:recency.
  • session_pin_boost, freshness_boost, and centrality stay zero in the response.
  • Spreading activation doesn't run.
  • PPR doesn't run; no cache, no hook.

Legacy-mode callers see zero behavioral change. Opt-in is per-SmartMemory instance; a service can run both modes side-by-side during a migration.

The four milestone arc

This feature shipped across four milestones under the CORE-MEMORY-DYNAMICS-1 initiative:

MilestoneWhat it added
M1aget_working_context() API + contract, observational ConsolidationRouter, MCP/SDK surface
M1bworking β†’ pending memory type rename (breaking change, contained)
M2aActivation primitives β€” boost, decay, retrieval-boost hook, ranking cutover, five activation-driven evolvers
M2bFlow expansion — ResolutionChainEvolver for Q→A edges, 5-tier progressive compression, workspace capacity cap with competitive inhibition
M3Surfacing router, session pins, spreading activation, Personalized PageRank, graph write-hook invalidation

The system is feature-complete as of M3. Additional refinements (LLM-based query classifier, async-refresh fallback, procedural:lookup / temporal:windowed strategy implementations, anchor drift warnings in-band) remain on the roadmap but don't change the overall shape.

On this page