Stop a full memory bank from shutting itself

Eviction ranked on use_count first. A memory written this turn has never
been used, so once every survivor in a full bank had been retrieved even
once, the newborn was the lowest row in the bank and was marked forgotten
by the same post-turn run that wrote it — one pass after the one that
embedded it, before retrieval ever saw it.

That state never ends, because counts only go up. The bank an adventure
happened to hold when it first filled is the bank it keeps for good, and
everything the story does afterwards is summarized, evicted and never
ranked. A test now plays four turns against a full bank and asserts the
survivors are the new memories, not the opening ones; before this it kept
the opening three and forgot all four.

Order on last-touch instead, with the count as the tiebreak. A new memory
carries the newest timestamp there is, so it is the safest row in the bank
rather than the most doomed, and it has until something outlives it to
prove itself — the "protect the newest" behaviour falls out of the
ordering rather than being a count of rows to spare. Little is given up:
a memory that is genuinely used stays recently-used by being retrieved,
so the two orders only disagree about memories that mattered once and
have not been wanted since.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Dvvqn9ZDR4ixeFPHNbww7
This commit is contained in:
parththakkar106
2026-08-18 23:22:00 +05:30
committed by Parth
co-authored by Claude Opus 5
parent 5783361124
commit c0cd6fa7ce
2 changed files with 88 additions and 17 deletions
+18 -1
View File
@@ -507,12 +507,29 @@ def _evict_over_capacity(
overflow = active - max(1, settings.memory_bank_capacity)
if overflow <= 0:
return
# Least recently touched goes first, and how often it was used only breaks
# a tie. The other way round — use_count first — shut the bank. A memory
# written this turn has never been used, so once every survivor had been
# retrieved even once the newborn was the lowest row in the bank and was
# evicted by the same post-turn run that wrote it, in the pass right after
# the one that embedded it. Counts only ever go up, so that state never
# ends: the bank an adventure happened to hold when it first filled is the
# bank it keeps for good, and everything the story does afterwards is
# summarized, marked forgotten, and never ranked.
#
# Recency does not have that hole, because a new memory carries the newest
# timestamp there is — it is the safest row in the bank rather than the
# most doomed, and it gets the whole span until something outlives it to
# prove itself. Little is given up by demoting the count: a memory that is
# genuinely used stays recently-used by being retrieved, so the two only
# disagree about memories that mattered once and have not been wanted
# since, which is what a full bank should be dropping anyway.
doomed = db.execute(
select(models.Memory.id)
.where(*in_this_bank, models.Memory.pinned.is_(False))
.order_by(
models.Memory.use_count,
func.coalesce(models.Memory.last_used_at, models.Memory.created_at),
models.Memory.use_count,
)
.limit(overflow)
).scalars().all()