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
@@ -138,6 +138,7 @@ from fastapi.testclient import TestClient
|
||||
from sqlalchemy import text
|
||||
|
||||
from app import auth, limits, memorybank, models, security, seed, tree, worldstate
|
||||
from app.context import cursors
|
||||
from app.database import Base, SessionLocal, engine, get_db
|
||||
from app.main import app
|
||||
from app.providers import PromptParts
|
||||
@@ -348,8 +349,11 @@ def add_rich_extras(db, args, rng: random.Random, user, adventure) -> None:
|
||||
adventure.scenario_id = scenario.id
|
||||
adventure.world_state = rich_world_state(schema, args.actions)
|
||||
adventure.script_state = rich_script_state(args.actions)
|
||||
# The post-turn passes SP3 rewrites only do anything when summarization is
|
||||
# on and the cursors are somewhere other than the start.
|
||||
# The post-turn passes only do anything when summarization is on and the
|
||||
# marks are somewhere other than the start. Written as positions here and
|
||||
# translated into anchors once the actions exist (see build_fixture) — a
|
||||
# position is what a database being migrated to SP3 still holds, so the
|
||||
# fixture carries both and they have to say the same thing.
|
||||
adventure.auto_summarize = True
|
||||
adventure.story_summary = RICH_SUMMARY
|
||||
adventure.memory_cursor = max(0, args.actions - 8)
|
||||
@@ -535,6 +539,14 @@ def build_fixture(args, rng: random.Random) -> tuple[int, int]:
|
||||
db.add(memory)
|
||||
|
||||
if args.rich:
|
||||
# The memory/summary marks as nodes, translated from the positions
|
||||
# `add_rich_layers` set now that there are actions to point at —
|
||||
# through the same call the v1 importer uses. A fixture stamped
|
||||
# LATEST never meets a migration, so if this is skipped it is the
|
||||
# one database whose marks are only positions.
|
||||
db.flush()
|
||||
cursors.anchor_at_position(adventure, cursors.MEMORY, adventure.memory_cursor)
|
||||
cursors.anchor_at_position(adventure, cursors.SUMMARY, adventure.summary_cursor)
|
||||
add_second_adventure(db, rng, user)
|
||||
_assert_live_variant_invariant(db, adventure.id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user