{"record":{"id":"276580ee040d1366","repo":"odysseus-dev/odysseus","slug":"message-not-found","errorCode":null,"errorMessage":"Message not found","messagePattern":"Message not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"routes/history/history_routes.py","lineNumber":363,"sourceCode":"        _verify_session_owner(request, session_id)\n        try:\n            body = await request.json()\n            msg_id = body.get(\"msg_id\")\n            content = body.get(\"content\")\n            if not msg_id or content is None:\n                raise HTTPException(400, \"msg_id and content are required\")\n\n            _reserve_message_uploads(request, content)\n\n            session = session_manager.get_session(session_id)\n            db = SessionLocal()\n            try:\n                db_msg = db.query(DbChatMessage).filter(\n                    DbChatMessage.id == msg_id,\n                    DbChatMessage.session_id == session_id,\n                ).first()\n                if not db_msg:\n                    raise HTTPException(404, \"Message not found\")\n\n                db_msg.content = content\n                meta = {}\n                if db_msg.meta_data:\n                    try: meta = json.loads(db_msg.meta_data)\n                    except (json.JSONDecodeError, ValueError): pass\n                meta['edited'] = True\n                db_msg.meta_data = json.dumps(meta)\n\n                # Update in-memory history by matching _db_id\n                for hmsg in session.history:\n                    hmeta = hmsg.metadata if isinstance(hmsg, ChatMessage) else hmsg.get('metadata')\n                    if isinstance(hmeta, dict) and hmeta.get('_db_id') == msg_id:\n                        if isinstance(hmsg, ChatMessage):\n                            hmsg.content = content\n                            hmsg.metadata['edited'] = True\n                        elif isinstance(hmsg, dict):\n                            hmsg['content'] = content","sourceCodeStart":345,"sourceCodeEnd":381,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/history/history_routes.py#L345-L381","documentation":"Raised by edit-message when the SQLAlchemy query filtering DbChatMessage.id == msg_id AND DbChatMessage.session_id == session_id returns no row. The message id must belong to the same session — a valid id from a different session also 404s (this doubles as an authorization boundary).","triggerScenarios":"Editing a message id from another session; editing a message whose DB row was already deleted (the delete-messages endpoint ran first in another tab); a message that exists only in memory because _persist_message failed silently; msg_id sent as a string when the column is an integer causing a type-mismatched filter.","commonSituations":"UI holds stale message ids after a fork/merge operation rewrote rows; two tabs editing the same conversation; message rows deleted by merge-last-assistant's _merge_continue_rows_to_delete while an edit dialog was open.","solutions":["Re-fetch the session history (GET /api/session/{id}) and use a currently listed message's _db_id","Verify the row exists and belongs to the session: SELECT id FROM chat_messages WHERE id = ? AND session_id = ?","Ensure msg_id is sent as the numeric DB id, not the client-side array index or an id from a different/forked session","If edits keep 404ing right after streaming, wait for the assistant message to be persisted before showing the edit affordance"],"exampleFix":"// before\nconst res = await fetch(`/api/session/${sid}/edit-message`, {method:'POST', body: JSON.stringify({msg_id: oldDbId, content})});\n// after\nconst hist = await (await fetch(`/api/session/${sid}`)).json();\nconst live = hist.messages.find(m => m._db_id === oldDbId);\nif (!live) { /* message was deleted/merged; refresh UI */ return; }\nconst res = await fetch(`/api/session/${sid}/edit-message`, {method:'POST', body: JSON.stringify({msg_id: live._db_id, content})});","handlingStrategy":"validation","validationCode":"const hist = await (await fetch(`/api/session/${sid}`)).json();\nconst live = hist.messages?.some(m => m._db_id === msgId);\nif (!live) { refreshTranscript(); return; }","typeGuard":"const isLiveMessage = (m, msgId, sid) => m && m._db_id === msgId && m.session_id === sid;","tryCatchPattern":"try { await editMessage(sid, msgId, content); }\ncatch (e) { if (e.status === 404) { await reloadHistory(sid); } else throw e; }","preventionTips":["Always edit by the _db_id from the freshest history fetch","After merge/fork/delete operations, refetch history before opening edit dialogs","Never reuse message ids across sessions"],"tags":["fastapi","http-404","sqlite","session-management","authorization"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}