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:
parththakkar106
2026-08-02 20:44:06 +05:30
co-authored by Claude Opus 5
parent be6dbb0f24
commit 906ba423d8
9 changed files with 734 additions and 22 deletions
+37
View File
@@ -392,6 +392,43 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
}
.story .action-tools button:hover { color: var(--text); }
/* Retry-history pager under an AI beat. Stays quiet until hovered — it's a
footnote on the message, not part of the prose. */
.variant-pager {
display: flex;
align-items: center;
gap: 4px;
margin-top: 6px;
font-family: var(--font-ui);
opacity: 0.45;
transition: opacity 0.15s;
}
.story .action:hover .variant-pager,
.variant-pager:focus-within { opacity: 1; }
.variant-pager button {
padding: 0 7px;
font-size: 0.9rem;
line-height: 1.5;
color: var(--text-dim);
background: transparent;
border: 1px solid transparent;
}
.variant-pager button:hover:not(:disabled) { color: var(--accent-bright); border-color: var(--border); }
.variant-pager button:disabled { opacity: 0.35; cursor: default; }
.variant-count {
font-size: 0.74rem;
color: var(--text-dim);
font-variant-numeric: tabular-nums;
min-width: 30px;
text-align: center;
}
.variant-note {
margin-left: 4px;
font-size: 0.72rem;
font-style: italic;
color: var(--accent-dim);
}
.action-edit { margin-bottom: 14px; }
/* Sized by AutoTextarea to fit the text being edited — an AI beat is usually
several paragraphs, and the old fixed 110px turned that into a keyhole.