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
- Track undo_count from each undo response and stop issuing further undos when it reaches 0
- Expose undo availability in the UI from the run's undo_count instead of always showing an enabled undo button
- 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
- Track undo_count from each response and stop at 0
- Drive undo availability from run.undo_count in the UI
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
- {RunBusyError message}
- {exc}
- not active
- cancel the active run before undoing memory edits
- PageIndex OSS preflight failed: {details}
AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27).
Data as JSON: /api/errors/ffd41f3a04591a87.
Report an issue: GitHub.