Skip to content

Aborted assistant turn persisted with zero content bricks the session: empty assistant message replayed to provider (400 "must not be empty") #37946

Closed as not planned

Description

@Oloompa

Description

A turn was aborted mid-stream (the TUI had frozen after an OS suspend/resume, so I aborted). OpenCode persisted the aborted assistant message with error: MessageAbortedError and only step-start / reasoning / patch parts — no text, no tool call.

From then on, every subsequent prompt replays this message in the history as an assistant message with empty content. Moonshot (via OpenRouter) rejects the whole request:

[Moonshot AI] Invalid request: the message at position 29 with role 'assistant' must not be empty (400)

It snowballs: each failed retry persists another empty assistant message with error: APIError, so the failing position advances (29 → 30 → …) and the session is permanently unusable. I had to manually delete the artifact messages from opencode.db to recover the session.

Expected behavior: an aborted/errored assistant turn with no serializable content should either not be persisted, or be filtered out when building the model messages (the assistant-side equivalent of the guard requested in #31046 for empty text parts).

Steps to reproduce

  1. Session with a strict provider (openrouter / moonshotai/kimi-k3)
  2. Abort a turn while the model is still in reasoning, before any text or tool part is emitted
  3. Send any new message → 400 "must not be empty"; each retry fails the same way and appends a new empty artifact message

Environment

  • OpenCode version: 1.18.3
  • OS: Linux
  • Plugins: oh-my-opencode

Related

Activity

  1. github-actions commented on Jul 20, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or closely related to existing issues not already mentioned. Please check:

  2. codeg-dev commented on Jul 22, 2026

    @codeg-dev

    I reproduced the strict-provider replay failure described here and opened #38270 with a targeted fix.

    The patch post-processes the final ModelMessage list for @ai-sdk/openai-compatible: if an assistant wire message has no non-empty text, it prepends a single-space text part. This covers the aborted step-start / reasoning-only shape from this issue, empty-text-only messages, tool-call-only turns, and per-step pieces created after step-start splitting without dropping tool-call or reasoning history.

    Validation includes 7 regression cases (43/43 tests passing), clean typecheck, and a replay of a previously bricked Kimi K3 session where the follow-up request now succeeds.

    This is intentionally broader than #33899: that PR only handles messages with tool calls and therefore leaves this issue's no-tool-call aborted reasoning-only shape unchanged.

  3. added a commit that references this issue on Aug 22, 2026
    5d79ade
  4. saltyfisher commented on Aug 26, 2026

    @saltyfisher

    Reproduced on opencode 1.18.19 (Windows, provider opencode-go, model kimi-k3), so this is still unfixed in the latest 1.x.

    Trigger in our case: an upstream connectivity failure mid-stream, not a manual abort:

    08:00:38  user asks a question, streaming starts (step 0)
    08:02:21  stream error: "Cannot connect to API" -> stream dies mid-generation
    08:02:25  opencode retries the stream
    08:03:10  step 1 starts, new assistant message msg_03d182f4 created, request sent
    08:03:17  upstream rejects the whole payload: 400 "the message at position 26 with
              role 'assistant' must not be empty" -> msg_03d182f4 stays persisted with
              ZERO parts
    08:04:28  next user turn replays the poisoned history -> same 400 -> yet another
              empty assistant row is persisted
    

    Amplifying effect worth noting: every failed retry adds another empty assistant message to the session, so the poison history grows and the session is permanently bricked — there is no way out from the UI.

    Storage detail (1.x SQLite): the empty rows are assistant rows in the message table of ~/.local/share/opencode/opencode.db with zero matching rows in part.

    Workaround (fixed both affected sessions for us): back up the DB, then delete the partless assistant messages:

    DELETE FROM part WHERE message_id IN (SELECT m.id FROM message m
      LEFT JOIN part p ON p.message_id = m.id
      WHERE json_extract(m.data, '$.role') = 'assistant'
      GROUP BY m.id HAVING COUNT(p.id) = 0);

    The proposed fix in #38270 (sanitize the outgoing payload) would mask the symptom, but it would not stop empty assistant rows from accumulating in the DB. Ideally opencode should also avoid persisting the assistant scaffold when the stream fails before any content arrives, or clean it up on retry.

  5. impptg commented on Aug 28, 2026

    @impptg

    Still reproduces on 1.18.23 (latest release) — I just hit the snowball variant: a rate-limited turn persisted a zero-part assistant message with error: APIError, and every subsequent request failed with the message at position 772 with role 'assistant' must not be empty, each retry appending yet another empty artifact message (391 → had to delete 3 rows from opencode.db to recover).

    The guards currently on dev (skip zero-part messages, skip errored messages, skip step-start-only messages) cover the zero-part case, but three shapes still replay as empty assistant content:

    1. Aborted turn with a patch part — the abort carve-out treats any non-step-start/reasoning part as content, so [step-start, reasoning, patch] survives; patch never serializes, leaving a reasoning-only message that OpenAI-compatible converters turn into content: "". This is the exact shape in this issue's report.
    2. Reasoning-only turn with no error field (crash/disconnect before the error is persisted) — the error filter never runs.
    3. Turn with only empty text parts (text: "").

    Verified against dev by calling MessageV2.toModelMessages directly with these shapes — all three still produce empty assistant messages.

    PR with a fix + regression tests: #45839

    The fix tightens the abort carve-out to count only model-visible parts (non-empty text or tool) and drops any assistant turn that produces no model-visible content at conversion time — so it also heals already-bricked sessions without a DB migration.

  6. impptg commented on Aug 31, 2026

    @impptg

    Same thing bricked several of my sessions (Kimi K3 behind a litellm gateway). Until the core fix in #45839 lands, you can work around it with a small plugin that hooks experimental.chat.messages.transform and sanitizes replayed history before conversion — it drops empty text parts and step segments that contain only reasoning (which OpenAI-compatible providers drop, producing the empty assistant message): https://github.com/impptg/opencode-fix-empty-assistant-messages

    For an already-bricked session: the stored history in ~/.local/share/opencode/opencode.db needs a cleanup (delete empty text parts / reasoning-only step segments of the offending assistant messages), then restart the client. Once #45839 is merged the plugin can be removed — conversion-time filtering also heals bricked sessions automatically.

  7. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions