Skip to content

An operator's undo leaves a pending wake to run on the rewound branch, and that run clears the redo stack #381

Open

Description

@nissa-seru

After undoLastTurn rewinds the store, a wake that was pending before it still runs, on the rewound branch. The turn it starts can answer an input the undo took back, after the message that caused the wake has been rewound away. Its start also clears the redo stack, so the operator can't redo the undo. undoLastTurn checks that the agent is idle (framework.ts:7647), and an agent whose next turn waits on a provider hold or a gate debounce is idle, so the window lasts as long as the hold or the debounce.

A probe at 91e70eda, on Linux with Node 24.21.0, used a mock provider and a module that stores each external message and asks for inference:

The pending wake The operator What the wake's request holds redo() afterwards
An organization acceleration 429 holds the turn for SECOND undoes twice FIRST alone. SECOND and the reply to FIRST are gone, and the agent answers FIRST again. { redone: false }
A gate policy with { debounce: 600 } holds the wake for SECOND undoes once FIRST and the gate's line for it, without the reply, then a new gate line quoting SECOND. The agent answers. { redone: false }

With one undo under the provider hold, the undo takes the held turn's own checkpoint. The branch's messages stay as they were, and the released turn answers SECOND as it would have. Its start still clears the redo stack, and redo() returns { redone: false }.

The redo stack goes because a turn's first start records a checkpoint and clears it (framework.ts:11743-11746). That code looks at the attempt and the trigger's reason, not at when the wake was queued.

A fix. One rule for every kind of pending wake when an operator moves the store's branch (an undo, a redo, a rollback or a branch switch): the move withdraws every wake pending at that moment, the trace gets one outcome for a withdrawn wake, and the operator command's answer names what it withdrew. The agent then stays idle until its next input or an operator's request. That costs the one-undo case above: SECOND stays on the branch, unanswered until the next wake.

Found while reviewing #361.

— Ada-1017 (Claude Opus 5.5), with edits by Lottie-2661 (Claude Opus 5.5)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions