anomalyco / anomalyco/opencode
Aborting a generation then editing/resending the message leaves an orphaned parentID, bricking the session view with a 404
@jlongster is already working on this.
Since Jul 28, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Aborting a generation then editing/resending the message leaves an orphaned parentID, bricking the session view with a 404
Summary
When a user aborts an in-progress assistant generation and then edits + resends the original user message, OpenCode removes the original user message (message.removed.1) but does not clean up the already-aborted assistant message that still references it via parentID. This leaves a dangling parentID.
When the Web UI later loads the session, it follows that parentID and issues GET /session/{ses}/message/{parentID} for the now-deleted message. The server returns 404 with an empty body, the fetch rejects, the message-list Promise.all throws, and the entire session view fails to render — the session becomes unopenable from the UI.
This is the same bug family as #16220 (abort bricks session), but a different failure mode: not an empty text block rejected by the model API, but a dangling parent reference that 404s in the client.
Environment
| OpenCode version | 1.18.2 |
| Deployment | Docker (debian-bookworm-slim), Web UI |
| Agent / Mode | plan |
| Provider / Model | zhipuai-coding-plan / glm-5.2 (variant max) |
| Storage | SQLite, event-sourced — ~/.local/share/opencode/opencode.db |
Steps to reproduce
Reconstructed from the event log of an affected session. Confirming the exact UI gesture, but the trigger is the abort → edit/resend flow.
- Open a session and send a user message.
- While the assistant is actively generating, abort it (Stop /
Esc). - Edit the original user message and resend it (this fires
message.removedon the original user message and starts a new turn). - Reload / reopen the session.
Actual behavior
The session view fails to load. The browser console shows:
Error: opencode server GET .../session/<session-id>/message/<removed-message-id> → 404 Not Found: (empty response body)
at ... (index-DZKREej7.js)
at async Promise.all (index 1)
The 404 has an empty response body, the message-list loader's Promise.all rejects, and the whole conversation can no longer be rendered.
Expected behavior
Removing a user message should either:
- also remove / re-parent child assistant messages that reference it via
parentID, or - resolve a missing
parentIDtarget gracefully (treat as no parent) instead of 404-throwing in the client.
Root cause (DB forensics)
Storage is event-sourced (event is the source of truth; message/part are projections). For the affected session, the relevant event sequence:
| seq | event type | target | note |
|---|---|---|---|
| 719 | message.updated.1 |
user msg …c3976 |
created (user turn starts) |
| 722 | message.updated.1 |
assistant msg …c393e |
created, parentID = …c3976 |
| 728 | message.updated.1 |
assistant msg …c393e |
completed with MessageAbortedError "Aborted" (~17.5s after start) |
| 730 | message.removed.1 |
user msg …c3976 |
original user message deleted (~5s after the abort) |
| 732 | message.updated.1 |
user msg …c6a63 |
new user message; conversation continues normally |
After seq 730:
- The
messageandpartrows for user message…c3976are gone (hence the 404). - The aborted assistant message
…c393eremains, and itsdata.parentIDstill points to the deleted…c3976— an orphaned reference. No compensating event re-parents or removes it.
The Web UI loads the message list, follows parentID, and GETs the deleted message → 404 (empty body) → throws → session bricked.
To find all such orphans in a session:
SELECT m.id AS assistant_msg,
json_extract(m.data, '$.parentID') AS dangling_parent
FROM message m
WHERE m.session_id = '<session-id>'
AND json_extract(m.data, '$.parentID') IS NOT NULL
AND json_extract(m.data, '$.parentID') NOT IN (SELECT id FROM message);
Note: parentID cannot be null
The API contract rejects a null parentID, so a client cannot self-heal by clearing the reference:
{ "name": "BadRequest",
"data": { "message": "Expected string, got null\n at [\"info\"][\"parentID\"]", "kind": "Body" } }
This makes the dangling-reference state unavoidable from the client side once the parent has been removed.
Workaround
Manual DB edit (projection-level): re-point the orphaned assistant message's parentID to a valid surviving message (or re-insert the removed user-message row from the event log).
UPDATE message
SET data = json_set(data, '$.parentID', '<valid-existing-message-id>')
WHERE id = '<orphaned-assistant-message-id>';
This restores the session immediately and survives a server restart (verified). Caveat: it edits the projection only; the event log still contains the orphan-producing events with no cleanup event, so a full projection rebuild could re-introduce the orphan.
Suggested fix
Pick one (or combine):
- On
message.removed, cascade: remove or re-parent every message whoseparentID== the removed message (and emit the correspondingmessage.removed/message.updatedevents). - Server-side: when resolving a message whose
parentIDtarget is missing, returnparentID: null(or a tombstone) instead of 404, so the client never throws. - Client-side: treat a 404 on a referenced parent as non-fatal (resolve to "no parent") instead of failing the whole message-list
Promise.all. - Additionally, consider relaxing the schema so
parentIDmay benull(currently rejected withExpected string, got null), which would make graceful self-healing possible.
Related
- #16220 — Cancelling agent mid-response bricks session (same abort-bricks-session family; different symptom: empty text block rejected by the model API, not a dangling
parentID). - #17717 — Retry unexpected
MessageAbortedError - #15853 — Aborting queued messages (app)
Additional context
- This was hit on the plan agent with a long-running generation that was aborted mid-stream, then edited and resent.
- The aborted assistant message carried
error: MessageAbortedError, consistent with an explicit user abort rather than a provider-side failure.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.