Problem
When an assistant turn is aborted before it emits any parts, OpenCode can persist a completed assistant message with MessageAbortedError and zero parts.
The backend already considers that turn finished, but the web UI has almost nothing to render for it, so the session can look blank or stuck even though it is no longer running.
This is a different shape from:
#17680 stale thinking state after interruption/restart
#15267 teardown race creating an empty continuation turn
This issue is specifically about the persisted message shape:
- assistant message exists
time.completed is set
error.name = MessageAbortedError
parts.length = 0
Concrete evidence
Live session investigated:
- session:
ses_30ad0ece4ffegDiKt3ugw5wurA
- final assistant message:
msg_cf98d282b0019d4mnTo0zcQl7H
Persisted row state:
time.completed = 1773714305166 (2026-03-16 19:25:05 PDT)
error.name = MessageAbortedError
error.data.message = "The operation was aborted."
part_count = 0
Additional checks on that session:
- orphan assistant rows with
time.completed IS NULL: 0
- pending/running tool parts:
0
So the backend state is not "still running". It is a completed aborted shell.
Why this is bad
The current UI path already treats MessageAbortedError specially, but when the assistant turn has zero visible parts, there is no substantive assistant content to show.
That makes the turn look blank or stuck, even though the backend has already finalized it.
Also, the persisted aborted error currently loses provenance. "The operation was aborted." does not tell us whether the source was:
- server restart
- user cancel
- client disconnect
- timeout
- some other abort path
Proposed fix
1. Persist abort provenance
Extend MessageAbortedError payload to include an optional structured source, for example:
server_restart
user_cancel
client_disconnect
timeout
unknown
Populate it at the abort sites and in restart recovery.
2. Render zero-part aborted assistant turns explicitly in the web UI
If an assistant turn is:
- completed
- aborted (
MessageAbortedError)
- and has zero visible parts
then render a compact interruption card/banner from the error payload instead of effectively showing an empty assistant shell.
3. Add a narrow invariant log
Log only when an assistant message is finalized with:
error != null
time.completed != null
- zero visible parts
This keeps diagnostics cheap and focused.
Acceptance criteria
- A completed aborted assistant message with zero parts no longer appears blank/stuck in the web UI.
- The UI shows a clear interrupted/aborted state for that turn.
MessageAbortedError can carry structured provenance for future diagnosis.
- Tests cover the zero-part aborted case.
Problem
When an assistant turn is aborted before it emits any parts, OpenCode can persist a completed assistant message with
MessageAbortedErrorand zero parts.The backend already considers that turn finished, but the web UI has almost nothing to render for it, so the session can look blank or stuck even though it is no longer running.
This is a different shape from:
#17680stale thinking state after interruption/restart#15267teardown race creating an empty continuation turnThis issue is specifically about the persisted message shape:
time.completedis seterror.name = MessageAbortedErrorparts.length = 0Concrete evidence
Live session investigated:
ses_30ad0ece4ffegDiKt3ugw5wurAmsg_cf98d282b0019d4mnTo0zcQl7HPersisted row state:
time.completed = 1773714305166(2026-03-16 19:25:05 PDT)error.name = MessageAbortedErrorerror.data.message = "The operation was aborted."part_count = 0Additional checks on that session:
time.completed IS NULL:00So the backend state is not "still running". It is a completed aborted shell.
Why this is bad
The current UI path already treats
MessageAbortedErrorspecially, but when the assistant turn has zero visible parts, there is no substantive assistant content to show.That makes the turn look blank or stuck, even though the backend has already finalized it.
Also, the persisted aborted error currently loses provenance.
"The operation was aborted."does not tell us whether the source was:Proposed fix
1. Persist abort provenance
Extend
MessageAbortedErrorpayload to include an optional structured source, for example:server_restartuser_cancelclient_disconnecttimeoutunknownPopulate it at the abort sites and in restart recovery.
2. Render zero-part aborted assistant turns explicitly in the web UI
If an assistant turn is:
MessageAbortedError)then render a compact interruption card/banner from the error payload instead of effectively showing an empty assistant shell.
3. Add a narrow invariant log
Log only when an assistant message is finalized with:
error != nulltime.completed != nullThis keeps diagnostics cheap and focused.
Acceptance criteria
MessageAbortedErrorcan carry structured provenance for future diagnosis.