HKUDS/DeepTutor · warning · HTTPException

nothing to undo

Error message

nothing to undo

What it means

HTTP 409 from POST /memory/runs/{run_id}/undo when undo_last returns None — the run is known and idle, but its undo stack is empty (every edit already undone, or the run produced no undoable events).

Source

Thrown at deeptutor/api/routers/memory.py:388

    return {"run_id": run_id, "cancelled": True}


@router.post("/runs/{run_id}/undo")
async def undo_run_edit(run_id: str):
    from deeptutor.services.memory.consolidator.runs import (
        RunBusyError,
        get_run_manager,
    )

    manager = get_run_manager()
    try:
        event = await manager.undo_last(run_id)
    except KeyError:
        raise HTTPException(status_code=404, detail="unknown run_id")
    except RunBusyError as exc:
        raise HTTPException(status_code=409, detail=str(exc))
    if event is None:
        raise HTTPException(status_code=409, detail="nothing to undo")
    run = manager.get(run_id)
    return {
        "run_id": run_id,
        "undone": True,
        "undo_count": len(run.undo_stack) if run else 0,
        "event": {"seq": event.seq, "ts": event.ts, **event.payload},
    }


@router.get("/runs")
async def list_runs(layer: str | None = None, key: str | None = None):
    from deeptutor.services.memory.consolidator.runs import get_run_manager

    lyr = _validate_layer(layer) if layer is not None else None
    if lyr and key is not None:
        _validate_doc_key(lyr, key)
    runs = get_run_manager().list_for(layer=lyr, key=key)
    return {"runs": [r.to_dict() for r in runs]}

View on GitHub (pinned to 3e82f13042)

Solutions

  1. Track undo_count from each undo response and stop issuing further undos when it reaches 0
  2. Expose undo availability in the UI from the run's undo_count instead of always showing an enabled undo button
  3. If you need to restore earlier state and undo is exhausted, start a fresh consolidation run instead

Example fix

// before
for (let i=0;i<n;i++) await fetch(`/memory/runs/${id}/undo`,{method:'POST'});
// after
let res = await (await fetch(`/memory/runs/${id}/undo`,{method:'POST'})).json();
while (res.undo_count > 0) {
  res = await (await fetch(`/memory/runs/${id}/undo`,{method:'POST'})).json();
}
Defensive patterns

Strategy: validation

Validate before calling

const run = await getRun(id);
if ((run.undo_count ?? 0) === 0) disableUndo();

Type guard

function hasUndo(run: {undo_count?: number}): boolean { return (run.undo_count ?? 0) > 0; }

Try / catch

try { const r = await undoRun(id); } catch (e) { if (e.status === 409 && e.detail === 'nothing to undo') stopUndoLoop(); else throw e; }

Prevention

When it happens

Trigger: Calling undo more times than there are undoable events, or undoing a run that made no edits to memory docs.

Common situations: Users mashing undo, clients not tracking undo_count from the response payload, or runs that only read/analyzed without writing.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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