Make a retry a node, not a rewrite
Every attempt at a turn is now its own row at the same (branch, depth), with `live` naming the one the story tells. The JSON repeating group on `actions.variants` is read one last time, by a migration that writes it out as the sibling rows it always described, and then goes unread. The snapshots turn around with it: an action carries the state it left behind rather than the state it started from, because attempts at one turn share a starting position and differ exactly in their outcome. Rolling back is "what the node in front left behind", one lookup on the path, and it is what undo and retry now both read. And the memory holdback goes. It existed because retry rewrote a row under a mark that had already moved past it; a retry writes a sibling now, and replacing what a coordinate says withdraws what was derived from it — the same repair undo and delete already made. The assembled prompt is still stored once per turn: it moves with the live flag, so a superseded attempt keeps only the few hundred bytes that were its own. Measured on the 600-action fixture: 700 rows for the same 600-turn story, prompt archive byte-identical at 0.50 MB, index 1.8 kB and page load 62.7 kB unmoved. 347 tests green. `tests/test_story_tree_baseline.py` and `tests/test_retry_variants.py` pass unmodified — SP4 was allowed to move the baseline for the variant-count semantics and did not need to. 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
c51531709d
commit
0a12d9cd47
@@ -294,8 +294,11 @@ def test_the_summary_folds_in_only_the_path_it_is_on(forked, monkeypatch):
|
||||
assert "on B" in prompt and "on C" in prompt
|
||||
assert "on A's own continuation" not in prompt
|
||||
assert "on the shared trunk" not in prompt # behind the mark
|
||||
# Caught up to the settled end of the story: C7 is retryable, C6 is not.
|
||||
assert cursors.SUMMARY.stored(adventure) == (ids["c"], 6)
|
||||
# Caught up to the end of the story. Until SP4 that was C6: the newest
|
||||
# action was held back because retrying it rewrote the row underneath the
|
||||
# mark. A retry writes a sibling now, and the withdrawal that follows takes
|
||||
# the mark back with it, so there is nothing to hold back.
|
||||
assert cursors.SUMMARY.stored(adventure) == (ids["c"], 7)
|
||||
|
||||
|
||||
def test_a_block_is_summarized_from_the_path_and_hung_off_its_last_node(
|
||||
@@ -317,13 +320,16 @@ def test_a_block_is_summarized_from_the_path_and_hung_off_its_last_node(
|
||||
monkeypatch.setattr(memorybank, "summary_provider", lambda s: stub)
|
||||
asyncio.run(memorybank._create_due_memories(adventure, settings, db))
|
||||
|
||||
# Two blocks of four from a path of eight, minus the held-back newest: one.
|
||||
[excerpt] = stub.excerpts
|
||||
assert "A5" not in excerpt, "a sibling's narration reached the summarizer"
|
||||
assert ["A0", "A1", "A2", "A3"] == [line for line in excerpt.split() if line[0] in "ABC"]
|
||||
# Two blocks of four from a path of eight, and since SP4 nothing is held
|
||||
# back, so both form in one pass.
|
||||
first, second = stub.excerpts
|
||||
assert "A5" not in first + second, "a sibling's narration reached the summarizer"
|
||||
assert ["A0", "A1", "A2", "A3"] == [line for line in first.split() if line[0] in "ABC"]
|
||||
assert ["B4", "B5", "C6", "C7"] == [line for line in second.split() if line[0] in "ABC"]
|
||||
made = db.query(models.Memory).filter_by(text="Memory 1.").one()
|
||||
assert (made.branch_id, made.depth) == (ids["a"], 3)
|
||||
assert cursors.MEMORY.stored(adventure) == (ids["a"], 3)
|
||||
# The mark ends up on the node the *second* block hangs off — the tip.
|
||||
assert cursors.MEMORY.stored(adventure) == (ids["c"], 7)
|
||||
|
||||
|
||||
# ------------------------------------------------------ the cost of forking
|
||||
|
||||
Reference in New Issue
Block a user