Skip to content

fix(llm): developer message after assistant turn opens a user message - #114

Merged
wolfy-j merged 1 commit into
masterfrom
fix/claude-developer-after-assistant
Sep 3, 2026
Merged

fix(llm): developer message after assistant turn opens a user message#114
wolfy-j merged 1 commit into
masterfrom
fix/claude-developer-after-assistant

Conversation

@wolfy-j

@wolfy-j wolfy-j commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Defect

claude/mapper.lua merged developer-role content into the previous message regardless of that message's role. Two consequences:

  1. Fabricated model output — after a truncated agent turn, the truncation-recovery feedback (<developer-instruction>...) was appended into the partial assistant message, attributing text to the model it never produced.
  2. Trailing-assistant prefill — the request then ends on an assistant turn. Models from Sonnet 4.6 onward (incl. the Claude 5 family) reject this with HTTP 400: This model does not support assistant message prefill. The conversation must end with a user message.

Observed in production

A kb_importer agent dataflow on claude-sonnet-5 died mid-import, twice, at the first truncated turn (the agent node's truncation recovery stores the partial action and a developer feedback observation — dataflow/src/node/agent/node.lua truncation branch — which the mapper then merged into the assistant message). Minimal reproduction against the API returns the same 400 (req_011CegoQokuK2cP4bx7pw8FP). On pre-4.6 models the pattern was accidentally functional because the API accepted the prefill.

Fix

Developer content merges only into a preceding plain user message. After an assistant message — or a tool_result — it opens a new user message, so recovery guidance reaches the model on the user turn and the request never ends on an assistant message.

Tests

  • 2 new mapper unit tests (fail on master: expected "user", got "assistant"), pinning: developer-after-assistant opens a user message, the assistant message stays byte-identical to what the model produced, and the mapped conversation never ends on an assistant turn.
  • 1 new integration test running the truncation-recovery conversation shape against claude-sonnet-5 through generate_handler (400 on master, passes with the fix).
  • Full llm suite: 1206 green, lint clean. Existing developer-merge tests (into user / image-only user messages) unchanged and passing.

https://claude.ai/code/session_01GGANLtqTDCWHbSYmycvjpV

The Claude mapper merged developer-role content into the previous
message regardless of its role. After a truncated agent turn the
previous message is the partial assistant text, so the truncation
feedback was glued into the assistant message: the model was credited
with text it never produced, and the request ended on an assistant
turn, which the API treats as prefill and models from Sonnet 4.6 on
reject with "This model does not support assistant message prefill"
(reproduced live: an agent flow died mid-import on claude-sonnet-5,
and a minimal trailing-assistant request returns the same 400).

Developer content now merges only into a preceding plain user message;
after an assistant message (or a tool_result) it opens a new user
message, so recovery guidance reaches the model on the user turn.

Claude-Session: https://claude.ai/code/session_01GGANLtqTDCWHbSYmycvjpV
@wolfy-j
wolfy-j merged commit dec012f into master Sep 3, 2026
21 checks passed
@wolfy-j
wolfy-j deleted the fix/claude-developer-after-assistant branch September 3, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant