popoto.fields.tombstone_prior¶
popoto.fields.tombstone_prior
¶
TombstonePriorStore — tombstones as negative prior (#494).
A tombstone (#491) is durable evidence that a particular kind of memory was
learned to be worthless. Until now nothing read that evidence, so the same
low-value content could be re-ingested, re-injected, re-dismissed and
re-forgotten indefinitely — the corpus relearned the same lesson forever. This
module makes the evidence transfer forward: it remembers how many times a
given content fingerprint has been buried, and WriteFilterMixin draws the
write-filter score of a matching new record down accordingly.
Why not the existing Bloom filter. ExistenceFilter already fingerprints
content and answers membership in O(1), which makes it the obvious candidate,
and it is the wrong one on three counts. Its on_save tokenizes the
fingerprint into words and its might_exist returns True when any single
token matches, so two records sharing one common word "match" — as a negative
prior that penalizes nearly every write. A Bloom filter cannot store a
per-entry burial count, which escalation requires. And its delete is a
documented no-op, so it cannot be bounded. ExistenceFilter is used here only
as the source of the fingerprint string, never as the matcher.
Matching is therefore exact: the fingerprint is whitespace-stripped, case-folded, and hashed. Two records match only when their fingerprints are equal under that normalization (or at a 2^-128 hash collision), which makes "a dissimilar record is not penalized" an exact property rather than a tuned threshold. Near-duplicate / paraphrase matching is deliberately out of scope; it needs a retrieval at write time and a false-positive policy of its own.
The digest, not the raw fingerprint, is the hash field name — a fingerprint may be user text, and this keyspace has no business holding content.
Like $TOMB:, the keyspace is deliberately kept OUTSIDE the model's own
keyspace so that no query, index scan, or key-set walk can surface it. It is
purely derived state: dropping it entirely degrades the system to its
pre-#494 behavior and loses nothing else.
Keyspace
$TOMBPRIOR:{Model}:burials — hash, fingerprint digest -> burial count
$TOMBPRIOR:{Model}:index — zset, fingerprint digest -> last burial ts
$TOMBPRIOR:{Model}:stats — hash, {penalized, drawdown_total}
Every Redis call here is best-effort: a failure logs a warning and degrades to
"no penalty" or "burial not recorded". A memory system whose save() dies
because a telemetry hash is unreachable is worse than one that occasionally
misses a drawdown.
TombstonePriorStore
¶
Owns the $TOMBPRIOR:{Model}:* keyspace for a single model class.
Sibling of TombstoneStore: same out-of-model-keyspace convention, same
pipelined-pairs construction, same get_REDIS_DB() accessor. Where
TombstoneStore archives what died, this store remembers how often a
given shape of content has died.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_class
|
Any
|
The Popoto Model class whose negative prior this store
manages. Only |
required |
Source code in src/popoto/fields/tombstone_prior.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 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 | |
keys()
¶
Return the (burials hash, recency index, stats hash) Redis keys.
Source code in src/popoto/fields/tombstone_prior.py
record_burial(fingerprint, ts)
¶
Record that fingerprint was buried at ts.
Increments the burial count and refreshes the recency index in one transactional pipeline, then enforces the retention bound.
Best-effort by contract: the caller (MemoryLifecycle.tombstone) has
already archived and removed the record, and a failure to record the
negative evidence must never roll that back.
Returns:
| Type | Description |
|---|---|
bool
|
True if a burial was recorded, False if there was no usable |
bool
|
fingerprint or the write failed. |
Source code in src/popoto/fields/tombstone_prior.py
burial_count(fingerprint)
¶
Return how many times fingerprint has been buried.
Returns 0 — meaning "no negative evidence, no penalty" — for a missing fingerprint, a missing digest, an unreachable Redis, or a stored value that does not read as a non-negative integer. Every one of those is a reason to leave the write alone rather than to fail it.
Source code in src/popoto/fields/tombstone_prior.py
note_penalty(before, after)
¶
Count one penalized write and the score it gave up.
Both counters move in one transactional pipeline so they can never disagree about how much drawdown the penalized writes account for.
Source code in src/popoto/fields/tombstone_prior.py
stats()
¶
Return the drawdown telemetry for this model.
Returns:
| Type | Description |
|---|---|
Dict[str, float]
|
|
Dict[str, float]
|
nothing has been penalized or the read fails, so a caller can |
Dict[str, float]
|
always render the numbers. |
Source code in src/popoto/fields/tombstone_prior.py
count()
¶
Return how many distinct buried fingerprints are tracked.
Source code in src/popoto/fields/tombstone_prior.py
purge_all()
¶
Drop the whole negative prior for this model: one DEL.
The count read is best-effort — the return value is a report, the delete is the job.
Source code in src/popoto/fields/tombstone_prior.py
digest_fingerprint(fingerprint)
¶
Normalize and hash a fingerprint, or return None if there isn't one.
Normalization is a whitespace strip plus a case fold, so trivial presentation differences do not defeat an exact match. An empty or whitespace-only fingerprint is not an identity — hashing it would give every content-less record the same digest and let them accumulate burials against each other — so it returns None and the caller skips.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fingerprint
|
Optional[str]
|
The ExistenceFilter fingerprint string, or None. |
required |
Returns:
| Type | Description |
|---|---|
Optional[str]
|
A 32-character hex digest, or None when there is no usable fingerprint. |
Source code in src/popoto/fields/tombstone_prior.py
penalty_for(burials)
¶
Return the multiplicative drawdown for burials prior burials.
Zero burials is no evidence, so the score passes through untouched. Each
subsequent burial compounds Defaults.TOMBSTONE_PRIOR_DECAY, floored at
Defaults.TOMBSTONE_PRIOR_FLOOR so suppression is asymptotic rather than
absolute — a memory buried for situational reasons is drawn down, never
annihilated.
Both constants are read at call time so a runtime override (e.g.
tests/benchmarks/overrides.apply_overrides) is observed.