Skip to content

fix(ai): place Anthropic system updates before the next assistant turn - #52568

Merged
rekram1-node merged 5 commits into
v2from
anthropic-update-placement
Oct 5, 2026
Merged

rekram1-node merged 5 commits into
v2from
anthropic-update-placement

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Issue for this PR

No linked issue.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Anthropic accepts a mid-conversation system message only right after a user turn (tool results count) and right before an assistant turn or at the end of the request. Consecutive system messages are accepted and judged together. The converter checked each update against its immediate neighbors, which lost real system messages in two common cases:

  • Instructions change while the session is idle. Core writes the update before the new prompt (assistant → update → user). An update after an assistant turn can't be native, so it was sent as wrapped <system-update> user text.
  • Two updates in a row. Each saw another system message next to it, so both were wrapped.

The converter now holds text updates and releases them together just before the next assistant turn, or at the end of the request. If the turn before that spot is a user turn, tool results, or a paused server-tool turn, they go out as native system messages; otherwise they are wrapped as before.

assistant → U → user                 →  assistant → user → system U
user → U1 → U2 → assistant           →  user → system U1 → system U2 → assistant
tool results → U → user (steer)      →  tool results → user → system U

Unchanged:

  • Models without native system updates keep the wrapped text in its current position, so their requests are byte-for-byte the same.
  • Effort-only markers stay where they are; Anthropic accepts them anywhere.
  • An update after an assistant turn with no user turn before the next assistant turn is still wrapped.

The order depends only on the stored history, so later requests keep the same prefix and prompt caching is unaffected. A session that already has these updates in history changes order once.

This also removes the Vertex-only rule from #43498 that wrapped any update directly after local tool results. Vertex returned 404 for that shape on Claude Opus 4.8 in August; it no longer does. On opencode-production (us and global, rawPredict and streamRawPredict), a native system message after tool results, at the end or followed by more turns, returned 200 with a complete stream and was followed on claude-opus-4-8, claude-opus-4-8@default, claude-opus-5-5, and claude-sonnet-5. Vertex now lowers updates exactly like the direct API.

How did you verify your code works?

  • New Anthropic tests for each reordered case, the wrapped fallback between two assistant turns, an unsupported model staying unchanged, and an effort marker next to a released update, and Vertex updates after tool results (terminal and in later history) lowering natively.
  • bun test and bun typecheck in packages/ai, test/session-runner.test.ts in packages/core, root bun run check.
  • Sent the compiled requests through the Vertex route for claude-opus-4-8 and claude-opus-5-5 in us and global: same six shapes plus an update after tool results. All 200.
  • Sent the compiled requests to the Anthropic API for claude-opus-5, claude-opus-4-8, claude-sonnet-5-5, claude-opus-5-5, and claude-fable-5-1: idle prompt, consecutive updates, both together, a steer after tool results, an effort marker plus update, and the wrapped fallback. All returned 200 and followed the updates.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

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