odysseus-dev/odysseus · error · HTTPException
Message not found
Error message
Message not found
What it means
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).
Source
Thrown at routes/history/history_routes.py:363
_verify_session_owner(request, session_id)
try:
body = await request.json()
msg_id = body.get("msg_id")
content = body.get("content")
if not msg_id or content is None:
raise HTTPException(400, "msg_id and content are required")
_reserve_message_uploads(request, content)
session = session_manager.get_session(session_id)
db = SessionLocal()
try:
db_msg = db.query(DbChatMessage).filter(
DbChatMessage.id == msg_id,
DbChatMessage.session_id == session_id,
).first()
if not db_msg:
raise HTTPException(404, "Message not found")
db_msg.content = content
meta = {}
if db_msg.meta_data:
try: meta = json.loads(db_msg.meta_data)
except (json.JSONDecodeError, ValueError): pass
meta['edited'] = True
db_msg.meta_data = json.dumps(meta)
# Update in-memory history by matching _db_id
for hmsg in session.history:
hmeta = hmsg.metadata if isinstance(hmsg, ChatMessage) else hmsg.get('metadata')
if isinstance(hmeta, dict) and hmeta.get('_db_id') == msg_id:
if isinstance(hmsg, ChatMessage):
hmsg.content = content
hmsg.metadata['edited'] = True
elif isinstance(hmsg, dict):
hmsg['content'] = contentView on GitHub (pinned to f9235ebbf1)
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
Example fix
// before
const res = await fetch(`/api/session/${sid}/edit-message`, {method:'POST', body: JSON.stringify({msg_id: oldDbId, content})});
// after
const hist = await (await fetch(`/api/session/${sid}`)).json();
const live = hist.messages.find(m => m._db_id === oldDbId);
if (!live) { /* message was deleted/merged; refresh UI */ return; }
const res = await fetch(`/api/session/${sid}/edit-message`, {method:'POST', body: JSON.stringify({msg_id: live._db_id, content})}); Defensive patterns
Strategy: validation
Validate before calling
const hist = await (await fetch(`/api/session/${sid}`)).json();
const live = hist.messages?.some(m => m._db_id === msgId);
if (!live) { refreshTranscript(); return; } Type guard
const isLiveMessage = (m, msgId, sid) => m && m._db_id === msgId && m.session_id === sid;
Try / catch
try { await editMessage(sid, msgId, content); }
catch (e) { if (e.status === 404) { await reloadHistory(sid); } else throw e; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/276580ee040d1366.
Report an issue: GitHub.