Skip to content

fix(opencode-plugin): strip trailing assistant messages to prevent Claude 4.x prefill error #17790

@superdav42

Description

@superdav42

Problem

opencode webui (especially on mobile) intermittently returns:

This model does not support assistant message prefill. The conversation must end with a user message.

This rejects requests on Claude Opus/Sonnet 4.x (Anthropic native, GitHub Copilot, OpenRouter, Bedrock, Vertex — all providers that proxy these models).

Root cause

Anthropic rejects conversations that end with an assistant message for Claude 4.x. This state occurs in opencode in three documented ways:

  1. Aborted/interrupted previous turn (sst/opencode#13768, PR #14772) — the aborted assistant has finish undefined, so the continuation gate at session/prompt.ts (lastAssistant2?.finish && !["tool-calls"].includes(...)) fails open and the next iteration calls the LLM with a trailing assistant still present. Common on mobile where flaky connections cause aborts.
  2. Continuation loop bug (sst/opencode#17982, PR #16921) — opencode 1.3.17 has a partial gate but it requires finish to be set.
  3. Max-steps hint (PR #14772) — when max steps is reached, opencode appends {role: "assistant", content: max_steps_default} after the plugin hook runs.

Three upstream PRs (#14772, #16921, #18091) all add stripTrailingAssistant() in provider/transform.ts. None are merged as of opencode v1.3.17 / v1.4.0.

Fix

Apply the same defensive strip at the plugin layer via experimental.chat.messages.transform, which runs immediately before MessageV2.toModelMessages() in session/prompt.ts:837128.

Files

  • NEW: .agents/plugins/opencode-aidevops/prefill-guard.mjs — pure module exporting createPrefillGuardHook(deps). Strips trailing assistant messages from output.messages only when safe (preserves messages with finish === "tool-calls" or active pending/running/completed tool parts so legitimate tool-call flows are untouched). Wrapped in try/catch so a bug here can never break the hook chain.
  • EDIT: .agents/plugins/opencode-aidevops/index.mjs — imports createPrefillGuardHook, composes a hook that runs messagesTransformHook first (so TTSR sees all assistants for rule scanning) then prefillGuardHook (so anything TTSR didn't fix gets stripped).

Reference pattern

Composition mirrors the existing messagesTransformHook wiring at index.mjs:204. The guard module follows the same shape as ttsr.mjs (factory + closure pattern with qualityLog injection).

Safety

  • Session DB is never modified — stripped messages still display in transcript history.
  • Tool-call flows are preserved (only assistants with no live tool calls are stripped).
  • Never throws — try/catch with diagnostic logging.
  • Composition order: TTSR runs first (it scans the last 3 assistants for rule violations and may append a synthetic user correction); guard runs second so if TTSR didn't append a correction AND the conversation still ends with an assistant, we strip it.

Verification

  1. node --check .agents/plugins/opencode-aidevops/prefill-guard.mjs (syntax)
  2. node --check .agents/plugins/opencode-aidevops/index.mjs (syntax)
  3. Restart opencode-web.service and confirm clean plugin load in journalctl --user -u opencode-web.service
  4. Send a message via mobile webui that previously triggered the prefill error — should now succeed. Stripped messages produce a journal line: [aidevops prefill-guard] stripped N trailing assistant message(s) ...

Acceptance criteria

  • Mobile webui no longer returns prefill error on Claude Opus/Sonnet 4.x
  • Tool-call flows on Claude 4.x continue working (not over-stripped)
  • Plugin loads without errors after restart
  • Journal shows guard activity when stripping occurs
  • No regression in other providers (the guard only strips, never adds)

Related upstream


aidevops.sh v3.6.162 plugin for OpenCode v1.3.17 with claude-opus-4-6 spent 1h 12m and 52,593 tokens on this with the user in an interactive session.

Metadata

Metadata

Assignees

Labels

auto-dispatchAuto-created from TODO.md tag

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions