Conversation
On OpenCode 2, adaptPayload() projects each tool-call/tool-result pair into a
native `{ type: "tool" }` part for the TS pipeline, and commit() maps them back
by looking each part up in `bridges`, a Map keyed by object identity. The drop
and edit-marker paths (clampCloneInPlace) deliberately rewrite a structuredClone
of the part and swap the clone into `parts`, so the identity lookup misses and
commit() emits the v1-shaped tool part unchanged. The OpenCode 2 request schema
rejects it ("Schema validation failed" in SessionModelRequest.prepare), and the
turn ends with no output. It starts once a session is large enough for tool
calls to be dropped, and every later turn in that session fails.
commit() now also resolves a part's bridge by (message id, callID), and a
message's host original by message id. The second covers trailing-blank
normalization, which replaces a message with a copy: that also lost the host
message (its id, and where its tool results belong). Keys are per message, so
two turns that reuse a callID keep their own call; the host's id-less
tool-result carriers keep an object key.
Tests go through the real createToolDropTarget truncate/editMarker paths on
adapted messages, including a reused callID and a message replaced by a copy
before commit(). Each fails without its part of the fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On OpenCode 2, once a session grows large enough for Magic Context to drop or edit-mark tool calls, every later turn in that session fails before it reaches the provider. The host logs:
The client shows "working" for a few seconds and then nothing. (The schema dump in that log mentions "Compaction requires either encrypted content or a summary", but that's just one member's annotation printed with the whole union. The actual issue is an
AnyOfatcontent[0]with no sub-issues: a part whosetypematches nothing.)Cause
adaptPayload()converts eachtool-call/tool-resultpair into a native{ type: "tool" }part for the TS pipeline.commit()converts them back by looking each part up inbridges, aMapkeyed by object identity.The drop and edit-marker paths (
clampCloneInPlace) deliberately rewrite astructuredCloneof the part and swap the clone intoparts, to keep the host's live objects untouched. Sobridges.get(part)misses the clone, andcommit()passes the v1-shaped part through unchanged:{"type":"tool","callID":"call_…","tool":"read","state":{"input":{"dropped":"[dropped §91§]"},"status":"completed","output":"[dropped §91§]"}}OpenCode 2's request schema rejects that. On the session where I hit this, 823 of these parts were in the request.
Trailing-blank normalization in
strip-content.tshas the same weakness at the message level. It replaces a message with a copy ({ ...message, parts: [...] }), sooriginals.get(message)misses too, and the host message (its id, and where its tool results go) is lost.Fix
commit()now also looks up:info, so the id survives. The key is per message, because two turns can reuse a callID. The host's id-less tool-result carriers keep an object key.Behaviour is unchanged whenever the identity lookups succeed.
Tests
src/v2/hooks/payload.test.ts(new) runs the realcreateToolDropTarget().truncate()and.editMarker()on messages produced byadaptPayload, then checks thatcommit()emits only V2 content types:tool-callplus atool-resultcarrying the sentinel;commit()keeps its id and its tool-result carrier;Each case fails without its part of the fix. I also checked it by mutation: taking out the id fallback for originals, or keying bridges by object only, fails the relevant tests.
bun test src/v2/: 42 pass.bun test --parallel: 4950 pass, 1 fail. The failure iscreateCtxSearchTools > explains why commit search is unavailable for a non-repository project, which fails the same way onmasterin my environment.Verified on a live host
I tested on OpenCode 2.0.7 with a copy of the affected store: a session of about 1.66M tokens with 823 dropped tool parts. Every turn failed with the error above. With this change (plus #477, needed for this store), the same session completes a turn at about 481k input tokens, and a context-hook probe finds no non-V2 parts.
Related: #477 (stores migrated from 1.x are refused as v1) and #475 (migration 85 relabels real OpenCode 2 rows).
Summary by cubic
Fixes OpenCode 2 sessions failing once Magic Context drops or edit-marks tool calls, so later turns reach the provider instead of dying with schema validation errors. Behavior is unchanged when identity lookups succeed.
adaptPayload()now maps cloned tool parts back to V2tool-call/tool-resultpairs by message ID and callID.Written for commit 9b2abc7. Summary will update on new commits.
The PR appears safe to merge, with the new fallbacks preserving V2 payload shapes across the documented cloning and message-copying paths.
Summary
This PR makes the OpenCode 2 payload adapter preserve host serialization after the transformation pipeline clones tool parts or copies their owning messages.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[V2 tool-call and tool-result] --> B[adaptPayload] B --> C[Native pipeline tool part] C --> D{Pipeline preserves identity?} D -->|Yes| E[Identity bridge lookup] D -->|Part or message cloned| F[Message ID and callID lookup] E --> G[commit] F --> G G --> H[V2 tool-call and tool-result]Reviews (1) · Last reviewed commit: "fix(opencode2): map cloned tool parts ba..."