HKUDS/DeepTutor · error · RunBusyError

cancel the active run before undoing memory edits

Error message

cancel the active run before undoing memory edits

What it means

RunManager.undo_last refuses to restore a document snapshot while the run that created it is still active. Undo pops a checkpoint from the run's undo_stack and rewrites the memory file, which would race with the run's own writes; therefore the run must be terminal (completed/cancelled) before undoing.

Source

Thrown at deeptutor/services/memory/consolidator/runs.py:210

        run._task = asyncio.create_task(self._drive(run, runner))
        return run

    async def cancel(self, run_id: str) -> bool:
        run = self._runs.get(run_id)
        if run is None or not run.active:
            return False
        run._cancel_flag.set()
        if run._task is not None and not run._task.done():
            run._task.cancel()
        return True

    async def undo_last(self, run_id: str) -> RunEvent | None:
        """Restore the document snapshot before the latest run write."""
        run = self._runs.get(run_id)
        if run is None:
            raise KeyError(run_id)
        if run.active:
            raise RunBusyError("cancel the active run before undoing memory edits")
        if not run.undo_stack:
            return None

        checkpoint = run.undo_stack.pop()
        path = Path(checkpoint.path)
        if checkpoint.existed:
            await asyncio.to_thread(_atomic_write, path, checkpoint.previous_content)
        else:
            await asyncio.to_thread(_remove_if_exists, path)

        return await self._emit(
            run,
            {
                "stage": "undo_applied",
                "run_id": run.id,
                "undo_id": checkpoint.id,
                "undo_depth": len(run.undo_stack),
                "layer": checkpoint.layer,

View on GitHub (pinned to 3e82f13042)

Solutions

  1. Await the run's completion (or cancellation) before calling undo_last; undo is only valid on finished runs
  2. If the run appears stuck active, cancel it first via the manager's cancel API, then call undo_last
  3. Verify run.active is False / run status is terminal before invoking undo

Example fix

// before
run = await manager.start(...)
# ... run emits writes but hasn't finished
await manager.undo_last(run.id)

// after
run = await manager.start(...)
await manager.wait_until_done(run.id)  # or await the runner
await manager.undo_last(run.id)
Defensive patterns

Strategy: validation

Validate before calling

run = manager.get(run_id)
if run is None or run.active:
    raise RuntimeError("run must be finished before undo")
event = await manager.undo_last(run_id)

Type guard

def can_undo(run) -> bool:
    return (not run.active) and bool(run.undo_stack)

Try / catch

try:
    await manager.undo_last(run_id)
except RunBusyError:
    await manager.cancel(run_id)
    await manager.undo_last(run_id)

Prevention

When it happens

Trigger: Calling undo_last(run_id) while run.active is True — e.g. undoing an edit immediately after a run's write events but before the run has fully finished, or undo_run_edit racing a long consolidation run.

Common situations: A UI 'undo' button wired to live run events; automated tests (test_undo_last_restores_previous_document) forgetting to await run completion before undoing; chaining undo immediately after start() without awaiting the runner.

Related errors


AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27). Data as JSON: /api/errors/8c958b43912edd2d. Report an issue: GitHub.