Tuning Surfacing
Task-oriented recipes for configuring SmartMemory's retrieval layer. Each recipe starts from a concrete goal ("I want X") and shows the minimum changes required.
Conceptual background: concepts/memory-dynamics.md
API reference: api/surfacing.md
Recipes
"I want to opt into the dynamics model"
Default SmartMemory uses legacy (M1a) retrieval. Flip one constructor arg:
from smartmemory import SmartMemory
memory = SmartMemory(surfacing_mode="router")That's it. The router, pin API, spreading activation, PPR centrality, freshness boost, and session-pin read guards are all active. Legacy-mode callers using the same workspace are unaffected — opt-in is per-instance.
"I want pinned items for my chat session"
Use pin_item() when the user (or your agent) decides an item is worth keeping top-of-mind for the current conversation.
memory = SmartMemory(surfacing_mode="router")
# Your agent decides "always reference the project brief in this session."
memory.pin_item("chat-2026-04-22-abc", "project-brief-item-id")
# Every subsequent get_working_context for this session force-includes
# the brief, regardless of what the user asks about.
resp = memory.get_working_context("chat-2026-04-22-abc", "anything")
assert any(i["item_id"] == "project-brief-item-id" for i in resp["items"])
# Clean up at session close.
memory.unpin_item("chat-2026-04-22-abc", "project-brief-item-id")Pins TTL at 24h so forgotten cleanups don't leak indefinitely.
"I want recent memories to rank higher"
The freshness_boost term adds a small additive lift for items under 24 hours old, decaying linearly to zero at the window edge. It's on by default in router mode with weight 0.2.
To make fresh items rank more aggressively, pass a higher weight via the smartmemory.activation.constants module at import time (no runtime config API yet):
# In your app's bootstrap, before constructing SmartMemory:
import smartmemory.activation.constants as _consts
_consts.FRESHNESS_BOOST_WEIGHT = 0.5 # up from 0.2
memory = SmartMemory(surfacing_mode="router")To widen the window (e.g. boost anything from this week, not just today):
_consts.FRESHNESS_WINDOW_HOURS = 168 # 7 days, up from 24Both are global — one setting per process.
"I want analytical queries to pull in graph neighbors"
Already happens automatically when surfacing_mode="router" is on and the router classifies your query as deep:multi-hop. No config needed.
Signals that trigger deep:multi-hop:
- Query contains
why,how,what caused,explain - Query has 2+ detected entities
- Query is longer than 80 characters
If you know the query is analytical and want to skip the classifier:
resp = memory.get_working_context(
session_id="s1",
query="explain this",
strategy="deep:multi-hop",
)"I want to make spreading activation more aggressive"
The default spread() walks 2 hops deep with a 20-neighbor fan-out per hop and a 0.5 damping factor. To tune, call spread() directly (the wrapper that get_working_context uses internally):
from smartmemory.search.spreading_activation import spread
# Wider net — more hops, less damping, larger frontier.
scores = spread(
memory,
seed_item_ids=["item-a"],
depth=3, # up from 2 — reaches further
max_fan_out=40, # up from 20 — larger frontier per hop
damping=0.7, # up from 0.5 — distant items contribute more
budget_ms=300, # up from 150 — allow more Cypher time
)Raising depth past 3 typically blows the budget on any non-trivial workspace. Prefer raising max_fan_out first.
To override the edge-type allowlist (e.g. only spread along co-retrieval edges):
scores = spread(
memory,
seed_item_ids=["item-a"],
edge_types=["CO_RETRIEVED", "ANSWERED_BY"],
)"My workspace is huge — will PPR cost me?"
Personalized PageRank materializes the workspace subgraph into memory for NetworkX. Workspaces with more than 10000 nodes are automatically skipped (PPR contributes nothing, centrality stays 0 in the composite, spreading and other signals still work).
The cutoff is configurable at call time:
from smartmemory.search.ppr import personalized_pagerank
scores = personalized_pagerank(
memory,
seed_item_ids=["item-a"],
budget_ms=500, # up from 200 for larger subgraphs
)To change the max-nodes threshold, pass it to the backend directly:
memory._graph.backend.materialize_workspace_subgraph(max_nodes=20000)
# Caller uses whatever subgraph is returned.PPR p99 latency on synthetic graphs (verified at release):
| Nodes | Median | p99 |
|---|---|---|
| 1,000 | ~2 ms | ~90 ms (JIT warmup outlier) |
| 5,000 | ~11 ms | ~12 ms |
| 10,000 | ~23 ms | ~25 ms |
The 10k cutoff is a safety margin, not a performance wall.
"I want to disable caching for a benchmark"
By default the router caches PPR results per (workspace, seed_hash) with a 5-minute TTL and write-through invalidation on graph mutations. To skip the cache for a benchmark pass, call PPR directly without a cache argument:
scores = personalized_pagerank(
memory,
seed_item_ids=["item-a"],
cache=None, # no cache read or write
)To force-invalidate all PPR state for a workspace (useful in tests):
cache = memory._ppr_cache()
if cache is not None:
cache.invalidate_workspace(memory.scope_provider.workspace_id)"I want to observe what the router decided"
After each get_working_context() call:
resp = memory.get_working_context("s1", "Why did we pick JWT?")
decision = memory.last_surfacing_decision
print(decision.strategy.value) # "deep:multi-hop"
print(decision.confidence) # 0.75
print(decision.reasons) # ["keyword:why"]
print(decision.signals) # {"query_length": 27, "word_count": 6, "entity_count": 0}Also surfaced in the response as resp["strategy_used"].
"I want to enable progressive compression of cold items"
Compression is a separate opt-in from surfacing — it runs in the background worker and compacts items whose activation has dropped below tier thresholds.
# Core-side: enable tier compaction on the SmartMemory instance.
memory = SmartMemory(
surfacing_mode="router",
tier_compaction=True, # M2b Slice B
)# Worker-side: gate job registration.
SMARTMEMORY_COMPACTION_ENABLED=trueWithout both flags the compaction sweep never registers and no items compress. User-authored content (origin tier 1) is always preserved at FULL regardless.
To rehydrate a compacted item back to full fidelity:
item = memory.rehydrate_item("some-item-id", target_tier="FULL")
# Content restored from the snapshot stored in metadata["snapshots"].
# needs_reembed=True is set so a future enrichment pass regenerates
# the embedding.Knobs reference
All activation/surfacing weights live in smartmemory.activation.constants:
| Constant | Default | What it controls |
|---|---|---|
PPR_WEIGHT | 0.5 | Centrality lift multiplier in the composite. centrality_lift = 1 + PPR_WEIGHT × centrality_norm. Max lift at centrality_norm=1 is 1.5×. |
FRESHNESS_BOOST_WEIGHT | 0.2 | Additive cap on the freshness boost for items < 24h old. |
FRESHNESS_WINDOW_HOURS | 24 | Linear-decay window for the freshness boost. |
SESSION_PIN_MULTIPLIER | 0.3 | session_pin_boost = pin_weight × SESSION_PIN_MULTIPLIER. |
ACTIVATION_FLOOR | 0.05 | Minimum activation used in composite's max(activation, floor) so cold items aren't zero-multiplied out of the running when relevance is high. |
BOOST_SIZE_RETRIEVE | 0.05 | Per-retrieval boost added to an item's activation score. |
BOOST_SIZE_SURFACE | 0.02 | Per-surfacing boost (less than retrieval — surfacing is less "intentional"). |
BOOST_SIZE_CORETRIEVE | 0.01 | Boost applied to co-retrieved items by the Hebbian evolver. |
DEFAULT_HALF_LIFE_HOURS | 720 | 30-day half-life for activation decay. |
DECAY_INTERVAL_SECONDS | 3600 | How often the worker's decay sweep runs. |
FLUSH_INTERVAL_SECONDS | 5 | How often queued retrieval boosts flush from Redis into persisted metadata. |
Redis keyspaces used (all on ACTIVATION_REDIS_DB, default DB 1):
sm:activation:pending:{workspace_id}:{item_id} — queued boosts
sm:activation:decay-lock:{workspace_id} — decay sweep lock
sm:activation:compaction-lock:{workspace_id} — compaction sweep lock
sm:session:{session_id}:pins — pin hash
sm:session:{session_id}:pinned_workspace — pin workspace binding
sm:ppr:{workspace_id}:{seed_hash} — PPR result cacheRelated documentation
concepts/memory-dynamics.md— the model explainedapi/surfacing.md— exact signatures and response shapesguides/performance-tuning.md— broader performance guidanceguides/advanced-features.md— other advanced operations