Keep every retried attempt instead of deleting it
Retry used to delete the last AI action and generate a replacement, so the discarded narration was simply gone. The row survives now: each attempt is appended to actions.variants with variant_index naming the live one, and a ChatGPT-style pager under the message browses them. Action.text still mirrors the active variant, so the context builder, memory bank, summarizer and export needed no changes. A variant carries only what differs between attempts -- the text, the reasoning, and the state it produced -- never the assembled prompt, which is identical across attempts of one turn and is the bulk of context_snapshot. Only the last message can be switched, restoring the script/world state that attempt produced; earlier turns were written as a continuation of whatever is active there, so theirs are read-only previews. Three things that would otherwise bite: - generate_turn now wraps _generate_turn and watches for a save sentinel. If the generator ends without it (provider error, empty reply, script stop, client hangup) it re-applies the previous variant -- otherwise a failed retry leaves rolled-back stats under un-rolled-back text. - Retry reuses the turn's own index rather than next_index, or the clock the world-state cooldowns run on advances on a re-run of the same turn. - Editing a message rewrites the active variant too, or paging away and back silently reverts the edit. Migrations 34/35 verified as an upgrade against a populated database, not just a fresh schema. test_state_revert's retry test asserted the old delete behaviour and was rewritten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UeQVy5bEjLhfgWNc27Efet
This commit is contained in:
co-authored by
Claude Opus 5
parent
be6dbb0f24
commit
906ba423d8
@@ -212,6 +212,13 @@ class Action(Base):
|
||||
state_before: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
# Phase 12: same idea for the RPG world_state, so undo/retry rolls it back too.
|
||||
world_state_before: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
# Retry history (AI actions): every attempt made for this turn, oldest
|
||||
# first, INCLUDING the active one. NULL/empty means never retried — the row
|
||||
# is its own only version. `variant_index` says which entry `text`,
|
||||
# `reasoning` and `context_snapshot` currently mirror; retry appends and
|
||||
# points here instead of deleting the row, so nothing is lost.
|
||||
variants: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||||
variant_index: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow)
|
||||
|
||||
adventure: Mapped[Adventure] = relationship(back_populates="actions")
|
||||
@@ -241,6 +248,12 @@ class Action(Base):
|
||||
out.append({"kind": "stat", "label": label, "delta": delta, "value": new})
|
||||
return out
|
||||
|
||||
@property
|
||||
def variant_count(self) -> int:
|
||||
"""How many attempts exist for this turn. 0 (not 1) when the action was
|
||||
never retried — the UI shows its pager only above 1 either way."""
|
||||
return len(self.variants) if isinstance(self.variants, list) else 0
|
||||
|
||||
|
||||
class Script(Base):
|
||||
__tablename__ = "scripts"
|
||||
|
||||
Reference in New Issue
Block a user