Skip to content

fix(opencode): ensure tool-call/tool-result pairing after normalizeMessages#22001

Open
bvironn wants to merge 1 commit intoanomalyco:devfrom
bvironn:fix/ensure-tool-result-integrity
Open

fix(opencode): ensure tool-call/tool-result pairing after normalizeMessages#22001
bvironn wants to merge 1 commit intoanomalyco:devfrom
bvironn:fix/ensure-tool-result-integrity

Conversation

@bvironn
Copy link
Copy Markdown

@bvironn bvironn commented Apr 11, 2026

Issue for this PR

Closes #21326

Type of change

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

What does this PR do?

Adds ensureToolIntegrity() in the provider transform pipeline, running after normalizeMessages for Anthropic/Bedrock/Vertex-Anthropic. It detects orphaned tool-call parts (no matching tool-result) and injects synthetic error results to keep the session alive.

Why here? normalizeMessages can drop messages with empty content, breaking tool-call/tool-result pairs. By the time the request leaves ProviderTransform.message(), orphaned tool-calls cause a permanent 400 from Anthropic — the corrupted history replays from SQLite on every retry, making the session unrecoverable.

The function is a no-op when pairs are intact (single pass, Map+Set lookups, zero allocation on happy path). It catches corruption from 6 independent vectors in the pipeline — normalizeMessages filtering (V1), error-skip logic in toModelMessages (V2), lost step boundaries during retry (V3), tool-error race conditions (V4), filterCompacted cutting pairs (V5), and the AI SDK producing empty assistant messages (V6). Full analysis in #21326 comment.

How did you verify your code works?

  • 130 existing transform tests pass (one updated for new pipeline ordering)
  • 8 new unit tests: matched pairs (no-op), single/multiple orphans, partial orphans appended to existing tool messages, empty input, string content, full end-to-end through ProviderTransform.message()
  • bun typecheck clean
  • Manual reproduction: corrupted a session simulating V1+V5, confirmed the fix prevents the 400

Checklist

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

Copilot AI review requested due to automatic review settings April 11, 2026 06:24
@github-actions github-actions bot added the needs:compliance This means the issue will auto-close after 2 hours. label Apr 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Great! I found several related PRs. Let me identify the most relevant ones:

Related PRs Found

  1. PR fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748) #16750 - fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748)

  2. PR fix(session): fix root causes and reconstruction of tool_use/tool_result mismatch (#16749) #16751 - fix(session): fix root causes and reconstruction of tool_use/tool_result mismatch (#16749)

  3. PR fix: blank assistant text - finish-reason regression after AI SDK v6 migration #20467 - fix: blank assistant text - finish-reason regression after AI SDK v6 migration

    • Related: Deals with blank assistant text issues that can break message pairing logic
  4. PR fix: prevent Kimi session poisoning from empty MCP tool-result text #14174 - fix: prevent Kimi session poisoning from empty MCP tool-result text

    • Related: Addresses session corruption from empty tool-result text, similar problem domain
  5. PR fix: handle dangling tool_use blocks for LiteLLM proxy compatibility #8497 - fix: handle dangling tool_use blocks for LiteLLM proxy compatibility

    • Related: Handles orphaned tool_use blocks without matching results

These are not exact duplicates of PR #22001, but they address related problems in the tool-call/tool-result pairing system. PR #22001 appears to be a comprehensive fix that consolidates lessons from these previous issues.

@github-actions github-actions bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Apr 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a post-normalization safety check to prevent Anthropic-style tool-call history corruption by ensuring every tool-call has a corresponding tool-result, even when upstream transforms drop/alter messages.

Changes:

  • Added ProviderTransform.ensureToolIntegrity() to detect orphaned tool-calls and inject synthetic error tool-results.
  • Wired ensureToolIntegrity() into ProviderTransform.message() after normalizeMessages() for Anthropic/Bedrock SDKs.
  • Expanded/updated provider transform tests to cover multiple orphan scenarios and the full normalize→repair pipeline.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/opencode/src/provider/transform.ts Introduces ensureToolIntegrity() and applies it after normalizeMessages() for specific providers.
packages/opencode/test/provider/transform.test.ts Updates an existing Anthropic normalization test expectation and adds a new test suite covering integrity repair cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

export function message(msgs: ModelMessage[], model: Provider.Model, options: Record<string, unknown>) {
msgs = unsupportedParts(msgs, model)
msgs = normalizeMessages(msgs, model, options)
if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock") {
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool integrity repair is only enabled for models using @ai-sdk/anthropic and @ai-sdk/amazon-bedrock. This omits the Vertex Anthropic provider (@ai-sdk/google-vertex/anthropic / providerID google-vertex-anthropic), which is treated as Anthropic elsewhere in this module and likely has the same tool-call/tool-result pairing requirement. Consider including that provider in the condition so Vertex Anthropic sessions can’t be corrupted the same way.

Suggested change
if (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/amazon-bedrock") {
if (
model.api.npm === "@ai-sdk/anthropic" ||
model.api.npm === "@ai-sdk/amazon-bedrock" ||
model.providerID === "google-vertex-anthropic"
) {

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already covered — the condition at line 332 includes model.providerID === "google-vertex-anthropic" alongside the npm checks for @ai-sdk/anthropic and @ai-sdk/amazon-bedrock. This matches the same triple-check pattern used by applyCaching below (lines 336-347).

The npm package for Vertex Anthropic is @ai-sdk/google-vertex/anthropic (a subpath export), so checking model.api.npm alone would miss it — that's why the providerID fallback is there.

@bvironn bvironn force-pushed the fix/ensure-tool-result-integrity branch from a7b2dae to 8521eb6 Compare April 11, 2026 06:33
@bvironn
Copy link
Copy Markdown
Author

bvironn commented Apr 11, 2026

CI Note: E2E timeouts are unrelated to this change

Both e2e (linux) and e2e (windows) failed with runner timeouts (31m / 33m exceeding the 30-minute limit). Unit tests pass on both platforms.

This PR only modifies the message transform pipeline (transform.ts) — it doesn't touch session lifecycle, TUI rendering, or any code path exercised by the Playwright E2E suite. The function itself is a no-op when tool-call/tool-result pairs are already matched (which they are in normal operation).

Happy to re-trigger if needed, but this appears to be a runner infrastructure issue rather than a regression.

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.

Interrupted tool calls permanently corrupt session history (orphaned tool_use without tool_result)

2 participants