Skip to content

fix(opencode): accept effort-marked Fable 5.1 tool continuations - #238

Open
iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:fix/fable-tool-continuation-effort-anchor
Open

iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:fix/fable-tool-continuation-effort-anchor

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Closes #237.

A Fable 5.1 turn dies with a synthetic 400 carrying Missing or invalid internal Fable 5.1 effort anchor whenever the model makes a tool call. Seven occurrences across three sessions in a week, in four message variants.

Root cause

Not what #237 guessed. That issue blamed boundaries carrying more than one user message. The plan lookup was never the problem — OpenCode keeps chat.headers.message.id bound to the originating user message throughout a tool loop, so the tracker resolves correctly on every continuation.

The anchor is pushed onto the user message that is current when the messages hook runs. Provider lowering then appends assistant tool_useuser tool_result after it, because a tool result is a user-role message in Anthropic's wire format. Validation required the anchor to sit on the last user message:

anchorMessageIndex !== lastUserMessageIndex   // → throw

Reproduced values at the throw:

anchorMessageIndex   2          lastUserMessageIndex  4
anchorsFound         1          markerCount           1
anchor boundary      msg_current   planned boundary   msg_current
anchor token == expected token · scopes identical

Everything correlated. Only the position differed, and it differed for a reason the check did not model.

Fix

An anchor may precede trailing user messages only when every one of them is a non-empty tool_result-only message whose tool_use_id appears in the immediately preceding assistant's tool_use blocks.

Still fail-closed, unchanged: plain user suffixes, holes, reorderings, mutations, scope mismatch, digest mismatch, anchor-token mismatch, multiple anchors.

Diagnosability

Equally important, and the reason this took two commits. The two throw sites emitted the identical string, and the throw happens before the dump path, so a production occurrence left zero trace in the log — all seven had to be reconstructed from the host database.

  • distinct checks: anchor_placement and anchor_token, with distinct messages
  • effortMarkerFailureResponse logs at warn with check code, anchor and planned boundary IDs, both indexes, anchors found, marker count, scopes, and match state
  • expected/found anchors are logged as SHA-256 hashes, never raw marker tokens

Verification

Reproduction: accepts authenticated effort history on a tool continuation — pre-fix 0 pass, 1 fail with anchor_placement.

The second commit exists because the first one's safety half was untested. Mutating the whole tool_result pairing predicate to return true passed the entire suite at 1897/0 — the existing rejection test builds user · user(anchor) · user(plain) with no assistant between, so it bails at an earlier guard and never reaches the pairing. The uncovered shape is an ordinary new user turn after an assistant reply.

Three tests now pin it, each independently mutation-proven:

test mutation that reddens it
rejects a plain user turn after an assistant reply whole pairing predicate → return true
rejects a tool result that does not match the preceding tool use drop toolUseIds.has(block.tool_use_id)
rejects a tool-result user message mixed with text .every(...).some(...)

Re-ran the original surviving mutation against the final tree: now 3 fail where it previously passed clean.

Gates: core 199/0 · opencode 1900/0 · pi 114/0 · typecheck clean.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes Fable 5.1 tool calls dying with a 400 error when the assistant makes a tool call. The anchor placement check was too strict — it required the effort anchor to sit on the last user message, but Anthropic's wire format appends tool results as user-role messages after it.

Bug Fixes

  • Now accepts trailing user messages after the anchor when every one is a tool_result-only message whose tool_use_id matches a tool_use block in the preceding assistant message.
  • Plain user suffixes, mismatched or mixed tool_result content, scope mismatches, and anchor token mismatches still fail closed.

Diagnosability

  • The two throw sites previously emitted the same string with no log output, leaving zero trace in production; they now use distinct anchor_placement and anchor_token check codes with distinct messages.
  • Refusals now log at warn with check code, boundary IDs, indexes, and match state — marker tokens are logged as SHA-256 hashes, never raw values.

Closes #237.

Written for commit 3251e64. Summary will update on new commits.

Review in cubic

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.

Fable 5.1 effort-marker correlation fails when a turn has more than one user message

1 participant