Refresh what a switch changes, not what a turn changes

A branch switch does not change the length of the story. It changes which
story it is. Four panels keyed on actions.length and so could not tell the
difference: the Branches panel drew one branch while the reader was already
on a second, Insights showed the prompt built for the path just left, the
script-state drawer kept the other line's numbers, and the Memory Bank did
not notice a deleted branch taking its memories with it.

Only the world-state drawer was right, and only because it happened to
carry stateKey already. They all key on the pair now, and deleting a branch
bumps it too — that is the one operation that changes what is stored
without a turn being played and without the story on the current path
moving by a single action.

tools/branch_fixture.py is the thing that could show it. The stress
fixture's world state is empty, so it cannot answer whether a switch puts
the scoreboard back, and its story is one branch. This builds a small
bootable adventure with a stat schema, a gold script, two takes on one turn
that differ by 35 hit points, a fork, and a memory on each side — with both
branches the same length on purpose, because equal length is precisely the
case a length-based key cannot see.

Verified in a browser with the drawer open: hp 60 to 95 and back, the bar
redrawn, the story swapped to the other take, Insights carrying the scratch
and not the beating. The Memory Bank deliberately does not change on a
switch: the drawer is adventure-wide so a memory is always findable to
delete, and retrieval is the path-scoped half.

396 tests, build clean, no new lint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015H5qiyiR7gtFQaoDphHZ3g
This commit is contained in:
parththakkar106
2026-08-18 19:14:07 +05:30
committed by Parth
co-authored by Claude Opus 5
parent 84827f0f37
commit 811368d048
4 changed files with 232 additions and 17 deletions
+20 -4
View File
@@ -1091,7 +1091,7 @@ function orderBranches(branches) {
// Delete is here rather than in some later subphase because nothing prunes a
// tree on its own — this panel is the first place a fork can be made, so it
// has to be the first place one can be unmade.
function BranchPanel({ advId, refreshKey, onSwitched, onError }) {
function BranchPanel({ advId, refreshKey, onSwitched, onTreeChanged, onError }) {
const [branches, setBranches] = useState(null)
const [failed, setFailed] = useState(null)
const [busyId, setBusyId] = useState(null)
@@ -1113,6 +1113,10 @@ function BranchPanel({ advId, refreshKey, onSwitched, onError }) {
try {
await work()
setTick((t) => t + 1)
// Deleting a branch takes its memories with it, and nothing else on the
// screen would hear about that — no turn is played, and the story on the
// current path does not change by a single action.
onTreeChanged()
} catch (err) {
onError(err.message)
} finally {
@@ -1841,8 +1845,12 @@ export default function Play() {
return (
<div className={`play-layout ${panel ? 'with-panel' : ''}`}>
{/* Both drawers read per-adventure state that a branch switch puts back,
so neither can key on the story's length alone: switching between two
branches whose windows are both full changes every number in here
without changing `actions.length` by one. */}
<WorldStateDrawer advId={id} refreshKey={`${actions.length}:${stateKey}`} />
<StatusDrawer advId={id} refreshKey={actions.length} />
<StatusDrawer advId={id} refreshKey={`${actions.length}:${stateKey}`} />
<div className="page play-page">
<div className="page-header">
<h1>{adventure.title}</h1>
@@ -2021,7 +2029,10 @@ export default function Play() {
onWorldStateChanged={() => setStateKey((k) => k + 1)} />
) : panel === 'memory' ? (
<MemoryPanel adventure={adventure} setAdventure={setAdventure}
refreshKey={actions.length} />
// The bank is adventure-wide, so a switch does not change it —
// but deleting a branch deletes the memories that hung off it,
// and that happens without a turn being played.
refreshKey={`${actions.length}:${stateKey}`} />
) : panel === 'scripts' ? (
<ScriptsPanel advId={id} />
) : panel === 'branches' ? (
@@ -2035,11 +2046,16 @@ export default function Play() {
// the two operations that move the head.
refreshKey={`${actions.length}:${stateKey}`}
onSwitched={adoptWindow}
onTreeChanged={() => setStateKey((k) => k + 1)}
onError={(message) => setToast({ text: message, isError: true })}
/>
) : (
// Insights is the prompt as it would be sent *now*, which is built
// from the story on the current path — so of everything on this
// screen it is the panel a branch switch changes most completely.
<InsightsPanel advId={id} inspectActionId={inspectActionId}
onClearInspect={() => setInspectActionId(null)} refreshKey={actions.length} />
onClearInspect={() => setInspectActionId(null)}
refreshKey={`${actions.length}:${stateKey}`} />
)}
</div>
)}