Put the shared state back when a turn is deleted
Deleting an AI reply removed the text and left everything the turn did to the numbers standing. `script_state` and `world_state` belong to the adventure rather than to the node, so the row going away takes nothing back. Undo, retry, a take and a branch switch all restore them; this endpoint never did. The visible symptom was the cooldown clock, which lives in `world_state._meta.last_changed` and holds a depth. A deleted turn left its depth there, and the turn played in its place is played at that same depth, so the referee refused the change as one that had happened this very turn — on a turn the story no longer contains. Delete the reply because you did not like the stat change it proposed, press Continue, and the same change comes back marked "changed too recently". A script's state stacked the same way: the gold a deleted turn paid out stayed paid, and the replacement turn paid it again. The restore reads the tip's own outcome rather than the deleted node's neighbour, which is what `switch` does. Deleting the newest turn then rewinds to the node in front of it, and deleting one from the middle of the story restores the state the adventure is already in, so the text goes and the numbers stay. The endpoint now takes the turn lock too, for the reason undo takes it: it writes the shared state, and a turn that is still generating is about to write it as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017imUKVPhqophVUZZwJK7ST
This commit is contained in:
@@ -2339,15 +2339,33 @@ def delete_action(
|
||||
action = db.get(models.Action, action_id)
|
||||
if action is None or action.adventure_id != adventure_id:
|
||||
raise HTTPException(404, "Action not found")
|
||||
# This works like undo. The turn is deleted with all of its attempts, and
|
||||
# whatever it produced is withdrawn. Nothing else is needed, because the
|
||||
# marks are depths, and a depth does not move when an action before it is
|
||||
# deleted.
|
||||
delete_turn(db, adventure, action)
|
||||
db.flush()
|
||||
db.expire(adventure, ["actions"])
|
||||
# Deleting the newest action moves the tip. Deleting an action in the
|
||||
# middle leaves a gap in the depths, which is intended. See
|
||||
# `_backfill_tree`.
|
||||
tree.refresh_head(db, adventure)
|
||||
db.commit()
|
||||
# The lock is held for the same reason undo holds it: this endpoint puts
|
||||
# the shared state back, and a turn that is still generating is about to
|
||||
# write it.
|
||||
acquire_turn_lock(adventure_id)
|
||||
try:
|
||||
# This works like undo. The turn is deleted with all of its attempts,
|
||||
# and whatever it produced is withdrawn. The marks are depths, and a
|
||||
# depth does not move when an action before it is deleted.
|
||||
delete_turn(db, adventure, action)
|
||||
db.flush()
|
||||
db.expire(adventure, ["actions"])
|
||||
# Deleting the newest action moves the tip. Deleting an action in the
|
||||
# middle leaves a gap in the depths, which is intended. See
|
||||
# `_backfill_tree`.
|
||||
tree.refresh_head(db, adventure)
|
||||
# The script state and the world state belong to the adventure, not to
|
||||
# the node, so deleting the node does not take back what it did to
|
||||
# them. Put them back to what the story now ends with, which is the
|
||||
# same restore a branch switch does.
|
||||
#
|
||||
# The world state carries the cooldown clock in `_meta.last_changed`,
|
||||
# and that clock is a depth. Leaving it set marked the deleted turn's
|
||||
# changes as having happened at the depth the next turn is played at,
|
||||
# so the referee refused them as changed too recently — on a turn the
|
||||
# story no longer contains. Deleting a middle action restores the tip's
|
||||
# own outcome, which is the state the adventure is already in.
|
||||
attempts.restore_state(adventure, db_tip(db, adventure))
|
||||
db.commit()
|
||||
finally:
|
||||
_active_turns.discard(adventure_id)
|
||||
|
||||
Reference in New Issue
Block a user