{"record":{"id":"54e7a90e84a8e526","repo":"usestrix/strix","slug":"each-update-must-include-todo-id","errorCode":null,"errorMessage":"Each update must include 'todo_id'","messagePattern":"Each update must include 'todo_id'","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"strix/tools/todo/tools.py","lineNumber":177,"sourceCode":"        if not stripped:\n            return []\n        try:\n            data = json.loads(stripped)\n        except json.JSONDecodeError as e:\n            raise ValueError(\"Updates must be valid JSON\") from e\n\n    if isinstance(data, dict):\n        data = [data]\n    if not isinstance(data, list):\n        raise TypeError(\"Updates must be a list of update objects\")\n\n    normalized: list[dict[str, Any]] = []\n    for item in data:\n        if not isinstance(item, dict):\n            raise TypeError(\"Each update must be an object with todo_id\")\n        todo_id = item.get(\"todo_id\") or item.get(\"id\")\n        if not todo_id:\n            raise ValueError(\"Each update must include 'todo_id'\")\n        normalized.append(\n            {\n                \"todo_id\": str(todo_id).strip(),\n                \"title\": item.get(\"title\"),\n                \"description\": item.get(\"description\"),\n                \"priority\": item.get(\"priority\"),\n                \"status\": item.get(\"status\"),\n            },\n        )\n    return normalized\n\n\ndef _normalize_bulk_todos(raw_todos: Any) -> list[dict[str, Any]]:\n    if raw_todos is None:\n        return []\n    data: Any = raw_todos\n    if isinstance(raw_todos, str):\n        stripped = raw_todos.strip()","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/tools/todo/tools.py#L159-L195","documentation":"Thrown by the todo-update normalizer in Strix's TodoWrite/TodoUpdate tool when an update object carries neither a 'todo_id' nor an 'id' key (or both are empty/None). The tool accepts 'id' as an alias, stringifies and strips it, then stores it as 'todo_id'. An item without an identifying key cannot be routed to _apply_single_update, so it is rejected before any state changes.","triggerScenarios":"Calling the todos update tool with updates=[{'title': 'new title'}] (no id), with {'todo_id': ''} or {'id': None}, or with a dict whose id key is misspelled ('todoid', 'task_id', 'uuid').","commonSituations":"An LLM agent composing update payloads from memory and dropping the id field; ids fetched from a prior list call but serialized as null; whitespace-only ids after copy-paste.","solutions":["Include 'todo_id' (or 'id') in every update object, e.g. {'todo_id': 'abc', 'status': 'completed'}","Verify ids come from a prior read of the todo list rather than being invented","Trim the id string before sending; an empty-after-strip id is treated as missing"],"exampleFix":"# before\nupdates = [{\"title\": \"finish report\"}]\n\n# after\nupdates = [{\"todo_id\": \"todo-3\", \"title\": \"finish report\"}]","handlingStrategy":"validation","validationCode":"def valid_updates(updates: list) -> bool:\n    return all(\n        isinstance(u, dict)\n        and bool(str(u.get(\"todo_id\") or u.get(\"id\") or \"\").strip())\n        for u in updates\n    )\n\nif not valid_updates(updates):\n    raise ValueError(\"each update needs a non-empty todo_id/id\")","typeGuard":"def is_update_object(v) -> bool:\n    return isinstance(v, dict) and bool(str(v.get(\"todo_id\") or v.get(\"id\") or \"\").strip())","tryCatchPattern":"try:\n    tool.update_todos(updates)\nexcept ValueError as e:\n    if \"todo_id\" in str(e):\n        rebuild updates from the last list_todos() result and retry once","preventionTips":["Always fetch ids from a prior list call; never synthesize them","Add a pre-send assertion that every update object has a non-empty todo_id","In agent prompts, require the id field in the update schema"],"tags":["validation","todo-tool","agent-input"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}