Skip to content

fix(compression): keep tool_result blocks first when aging annotates a turn - #12920

Merged
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.51from
ntdatt812:fix/aging-tool-result-order
Sep 10, 2026
Merged

diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.51from
ntdatt812:fix/aging-tool-result-order

Conversation

@ntdatt812

Copy link
Copy Markdown
Contributor

What

replaceTextContent() prepends the aging annotation when the message has no
text block to replace:

if (!replaced) {
  return { ...msg, content: [{ type: "text", text: newText }, ...msg.content] };
}

A user turn that answers a tool_use with a single tool_result block hits
exactly that branch, so aging rewrites it to ["text", "tool_result"]. The
Anthropic Messages API requires the tool_result blocks to lead that turn, so
upstream rejects the entire request:

messages.N: `tool_use` ids were found without `tool_result` blocks immediately after

This PR appends the annotation when the turn carries a tool_result, and keeps
prepending for every other shape, so no existing ordering changes.

Verifying the report first (#12890)

Confirmed on the tip, at the call site rather than from the description:

  • progressiveAging.ts::applyAging → setContent → replaceTextContent. A
    tool-result-only turn has extractTextContent(content) === "", so replaced
    stays false and the prepend branch runs.
  • Reproduced through applyAging (not by calling the helper directly): a turn at
    distanceFromEnd = 5 lands in the fullSummary tier and comes back as
    ["text", "tool_result"].

The report's suggested one-line fix (append unconditionally) would also move the
annotation for messages that carry no tool_result — images, for one — so the
condition is scoped to the shape that upstream actually constrains.

Test

tests/unit/compression/aging-tool-result-order-12890.test.ts

  1. Wiring: runs a real 8-message conversation through applyAging() and asserts
    the aged turn's block order is ["tool_result", "text"] and that the trailing
    block is the [COMPRESSED:aging:…] annotation. It goes through the aging entry
    point, not replaceTextContent in isolation.
  2. Guard: a turn holding only an image block still gets ["text", "image"].

Mutation

mutation result
!replaced branch back to the unconditional prepend (the shipped bug) ✖ keeps tool_result first through applyAging — got ["text","tool_result"]
!replaced branch to an unconditional append (the report's suggestion) ✖ still puts the annotation first when the turn carries no tool_result

Each mutation kills a different test, so neither branch of the new condition is
unpinned.

Regression

The 7 test files that exercise messageContent.ts / progressiveAging.ts /
aggressive.ts — aggressive-fidelity, aggressive, compression-aggressive,
golden-eval, progressiveAging, compression-aggressive-spare-last-user, plus
the new file: 54 pass, 0 fail.

Closes #12890

…a turn

replaceTextContent() prepends the "[COMPRESSED:aging:...]" annotation when a
message carries no text block to replace. A user turn that answers a tool_use
with a single tool_result block therefore becomes ["text", "tool_result"], and
the Anthropic Messages API rejects the whole request:

  messages.N: `tool_use` ids were found without `tool_result` blocks
  immediately after

Append the annotation instead when the turn contains a tool_result, and keep
prepending for every other shape so existing ordering is untouched.

Closes diegosouzapw#12890
@diegosouzapw
diegosouzapw merged commit e1a1290 into diegosouzapw:release/v3.8.51 Sep 10, 2026
8 of 16 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…a turn (diegosouzapw#12920)

Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
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.

fix(backend): Compression aging inserts text block before tool_result → Anthropic 400 "tool_use ids without tool_result"

2 participants