Mark the story with a node, not with a count
The memory bank and the story summary each kept a cursor: how many story actions they had already covered. A count is a position in a list, and this list moves — delete an action in front of the mark and every later one slides down a slot, so the mark now covers one it has never read. All the cursor bookkeeping existed to patch that up. Both marks are now (branch_id, depth): the node up to and including which the work is done. A depth is a coordinate along a path, not an offset into a list, so nothing in front of it can move it. That deletes rather than rewrites `position_of_index`, `note_action_removed`, `_rewind_cursors_to_index`, `prune_dangling_memories` and the every-pass clamp in `run_post_turn`. A memory hangs off the node its block ends on, so a fork inherits its ancestors' memories without copying any, and retrieval selects through the branch clause over the *whole* lineage — recall is long-range by definition and cannot be windowed. Measured: 1,807 B on a story forked twenty times against 1,823 B on a flat one of the same length. Migrations 53-56 translate the old counts into nodes. They rewrite `adventures` and not `actions`, so this one needs no VACUUM FULL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Dvvqn9ZDR4ixeFPHNbww7
This commit is contained in:
committed by
Parth
co-authored by
Claude Opus 5
parent
c7b6a46a8a
commit
c51531709d
@@ -140,24 +140,29 @@ def test_undo_prunes_memory_covering_removed_actions(db):
|
||||
assert texts == {"k"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- prune helper
|
||||
# -------------------------------------------------------- withdrawing a node
|
||||
|
||||
def test_prune_dangling_memories_counts_and_removes(db):
|
||||
def test_forget_node_withdraws_only_what_that_node_produced(db):
|
||||
"""Phase 14 SP3: a memory hangs off the node its block ends on, so removing
|
||||
a node is a lookup rather than a scan for memories that have fallen off the
|
||||
end of the story."""
|
||||
user, adv = _make_adventure(db, {})
|
||||
_add(db, adv, 0, "do")
|
||||
_add(db, adv, 1, "ai")
|
||||
second = _add(db, adv, 1, "ai")
|
||||
db.add_all([
|
||||
models.Memory(adventure_id=adv.id, text="live", source_start=0, source_end=1),
|
||||
models.Memory(adventure_id=adv.id, text="dead", source_start=2, source_end=5),
|
||||
models.Memory(adventure_id=adv.id, text="hangs off node 1",
|
||||
source_start=0, source_end=1),
|
||||
models.Memory(adventure_id=adv.id, text="hangs off node 0",
|
||||
source_start=0, source_end=0),
|
||||
])
|
||||
db.commit()
|
||||
|
||||
removed = memorybank.prune_dangling_memories(adv, db)
|
||||
removed = memorybank.forget_node(db, adv, second)
|
||||
db.commit()
|
||||
db.refresh(adv) # expire_on_commit=False: reload the memories collection
|
||||
|
||||
assert removed == 1
|
||||
assert {m.text for m in adv.memories} == {"live"}
|
||||
assert {m.text for m in adv.memories} == {"hangs off node 0"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- snapshot
|
||||
|
||||
Reference in New Issue
Block a user