{"record":{"id":"ffd41f3a04591a87","repo":"HKUDS/DeepTutor","slug":"nothing-to-undo","errorCode":null,"errorMessage":"nothing to undo","messagePattern":"nothing to undo","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"warning","filePath":"deeptutor/api/routers/memory.py","lineNumber":388,"sourceCode":"    return {\"run_id\": run_id, \"cancelled\": True}\n\n\n@router.post(\"/runs/{run_id}/undo\")\nasync def undo_run_edit(run_id: str):\n    from deeptutor.services.memory.consolidator.runs import (\n        RunBusyError,\n        get_run_manager,\n    )\n\n    manager = get_run_manager()\n    try:\n        event = await manager.undo_last(run_id)\n    except KeyError:\n        raise HTTPException(status_code=404, detail=\"unknown run_id\")\n    except RunBusyError as exc:\n        raise HTTPException(status_code=409, detail=str(exc))\n    if event is None:\n        raise HTTPException(status_code=409, detail=\"nothing to undo\")\n    run = manager.get(run_id)\n    return {\n        \"run_id\": run_id,\n        \"undone\": True,\n        \"undo_count\": len(run.undo_stack) if run else 0,\n        \"event\": {\"seq\": event.seq, \"ts\": event.ts, **event.payload},\n    }\n\n\n@router.get(\"/runs\")\nasync def list_runs(layer: str | None = None, key: str | None = None):\n    from deeptutor.services.memory.consolidator.runs import get_run_manager\n\n    lyr = _validate_layer(layer) if layer is not None else None\n    if lyr and key is not None:\n        _validate_doc_key(lyr, key)\n    runs = get_run_manager().list_for(layer=lyr, key=key)\n    return {\"runs\": [r.to_dict() for r in runs]}","sourceCodeStart":370,"sourceCodeEnd":406,"githubUrl":"https://github.com/HKUDS/DeepTutor/blob/3e82f130422a813cdd73c10b21a44e9325f5821a/deeptutor/api/routers/memory.py#L370-L406","documentation":"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).","triggerScenarios":"Calling undo more times than there are undoable events, or undoing a run that made no edits to memory docs.","commonSituations":"Users mashing undo, clients not tracking undo_count from the response payload, or runs that only read/analyzed without writing.","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"],"exampleFix":"// before\nfor (let i=0;i<n;i++) await fetch(`/memory/runs/${id}/undo`,{method:'POST'});\n// after\nlet res = await (await fetch(`/memory/runs/${id}/undo`,{method:'POST'})).json();\nwhile (res.undo_count > 0) {\n  res = await (await fetch(`/memory/runs/${id}/undo`,{method:'POST'})).json();\n}","handlingStrategy":"validation","validationCode":"const run = await getRun(id);\nif ((run.undo_count ?? 0) === 0) disableUndo();","typeGuard":"function hasUndo(run: {undo_count?: number}): boolean { return (run.undo_count ?? 0) > 0; }","tryCatchPattern":"try { const r = await undoRun(id); } catch (e) { if (e.status === 409 && e.detail === 'nothing to undo') stopUndoLoop(); else throw e; }","preventionTips":["Track undo_count from each response and stop at 0","Drive undo availability from run.undo_count in the UI"],"tags":["memory","undo","http-409","empty-stack"],"backgroundTag":"invalid-state-transition","analyzedSha":"3e82f130422a813cdd73c10b21a44e9325f5821a","analyzedAt":"2026-08-27T06:57:25.364Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}