popoto.recipes.default_memory¶
popoto.recipes.default_memory
¶
DefaultMemory -- the batteries-included agent-memory model (issue #513).
The agent-memory quickstart teaches a progressive schema: five levels of
fields you assemble yourself. That ladder stays, but it should not be the
first thing a new adopter writes. Following Level 1 verbatim into
:class:~popoto.recipes.subconscious_memory.SubconsciousMemory produced
silent query-blind retrieval, because a model without a BM25Field
makes ContextAssembler(retrieval_mode='auto') resolve to the
composite path, which ignores the query text entirely.
DefaultMemory is the shipped answer: import it and you get the
benchmarked configuration -- query-sensitive lexical retrieval, decay,
confidence, and an association graph -- with no schema authoring::
from popoto.recipes import DefaultMemory, SubconsciousMemory
sm = SubconsciousMemory(agent_id="agent-1")
Field choices and why:
memory_id (AutoKeyField)
A generated key so callers never invent one.
agent_id (KeyField)
Partition key. Every other index partitions by it, and an explicit
.filter(agent_id=...) query always honors that partition -- but
the default lexical/BM25 retrieval path does not yet filter by it
(#576), so two
agents sharing one Redis via the default loop can retrieve each
other's memories. Not a project-isolation boundary today.
content (StringField)
The memory text. Also the BM25Field source and the field the
content-first injection format reads.
importance (FloatField)
Base score for time decay. 1.0 default means "unweighted".
relevance (DecayingSortedField)
Recency * importance, partitioned by agent. The single index in the
benchmarked score_weights ({"relevance": 1.0}).
confidence (ConfidenceField)
Beta-style certainty updated by ObservationProtocol outcomes.
content_bm25 (BM25Field)
The field that makes retrieval query-sensitive. Its presence is what
flips retrieval_mode='auto' from composite to lexical.
associations (CoOccurrenceField)
Entity/record co-occurrence graph. Feeds the graph arm of lexical
retrieval and receives write-time entity links from
SubconsciousMemory.extract_memories().
NeverRecordMixin
The never-record firewall (#561). Included where WriteFilterMixin
is not, and the difference is not inconsistency: the write filter drops
records it guesses are unimportant, which is the wrong default because
a wrong guess is silent data loss. The firewall drops content that is
deterministically a credential or explicitly marked off-the-record,
where a wrong guess costs one memory and the alternative costs a leaked
secret. It also drops loudly -- every drop increments an auditable
per-reason counter (DefaultMemory.never_record_counts()).
This matters most because the Claude Code / Codex / Hermes / OpenClaw
harness writes through this model on every turn using raw turn ingestion
(#515), so a pasted API key would otherwise be persisted verbatim.
Deploy-level escape hatch: POPOTO_NEVER_RECORD_DISABLE=1.
Deliberately not included:
WriteFilterMixin
It silently discards records below _wf_min_threshold (save()
returns False). Silent data loss is the wrong default for a
first-run model; the quickstart introduces it at Level 2 where the
behavior is explained.
EmbeddingField
Requires an embedding provider (an API key or a local Ollama), so it
cannot be a zero-configuration default. Adding one to a subclass
flips auto from lexical to hybrid with no call-site change.
All numeric behavior is left to the field defaults, which read from
popoto.fields.constants.Defaults -- this module pins no constants of
its own (see the Defaults docstring for the convention).
Escape hatch: DefaultMemory is a single shared class, so every
importer writes into the DefaultMemory:* keyspace. Applications that
need their own keyspace (or extra fields) subclass it::
class ProjectMemory(DefaultMemory):
pass # keys become ProjectMemory:*
EVICTION_COUNTER_PREFIX = '$popoto_memory:counter'
module-attribute
¶
Redis key prefix for the durable eviction report (#596).
Duplicates popoto.integrations.service.COUNTER_KEY_PREFIX on purpose:
recipes/ must not import from integrations/, so the string is
restated here and a test asserts the two are equal. Choosing the counter
prefix means MemoryService._read_counters() already surfaces the number
in status(), the MCP memory_status tool, and popoto-memory doctor.
Contract: {prefix}:{agent_id}:evicted records the cap selected for
eviction, not records deleted. It is incremented by excess before the
delete loop runs, and the loop can legitimately delete fewer (the saving
record's own key is skipped, a missing hash is routed to an orphan purge,
and a mid-loop error aborts). The invariant is counter >= records actually
deleted, with equality on the clean path.
DefaultMemory
¶
Bases: NeverRecordMixin, AccessTrackerMixin, Model
Batteries-included agent memory: query-sensitive retrieval, no schema.
Equivalent to the quickstart's Level 4 model minus WriteFilterMixin.
ContextAssembler(retrieval_mode='auto') resolves to "lexical"
over this model, so query cues actually rank results.
Example::
from popoto.recipes import DefaultMemory
DefaultMemory(
agent_id="agent-1",
content="Deploy uses blue-green with automatic rollback",
importance=0.9,
).save()
results = DefaultMemory.query.filter(agent_id="agent-1").top_by_decay(n=5)
Source code in src/popoto/recipes/default_memory.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | |
save(pipeline=None, *args, **kwargs)
¶
Save, then evict the stalest records past _max_records_per_agent.
Staleness is the relevance decay timestamp, so a memory that is
touched (recalled, acted on) stays; one nobody has refreshed goes
first. Eviction is a full delete() so every index is cleaned.
Costs one ZCARD per save when under the cap.
Source code in src/popoto/recipes/default_memory.py
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | |