anomalyco / anomalyco/opencode

Aborting a generation then editing/resending the message leaves an orphaned parentID, bricking the session view with a 404

Open
#39,260 1 comment 0 reactions 1 assignee View on GitHub

@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.

  1. Open a session and send a user message.
  2. While the assistant is actively generating, abort it (Stop / Esc).
  3. Edit the original user message and resend it (this fires message.removed on the original user message and starts a new turn).
  4. 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 parentID target 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 message and part rows for user message …c3976 are gone (hence the 404).
  • The aborted assistant message …c393e remains, and its data.parentID still 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):

  1. On message.removed, cascade: remove or re-parent every message whose parentID == the removed message (and emit the corresponding message.removed / message.updated events).
  2. Server-side: when resolving a message whose parentID target is missing, return parentID: null (or a tombstone) instead of 404, so the client never throws.
  3. 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.
  4. Additionally, consider relaxing the schema so parentID may be null (currently rejected with Expected 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.