fix: keep parallel tool results consecutive before carried image messages - #33
Merged
Merged
Conversation
An assistant turn can issue several parallel tool calls and each tool result can carry images (e.g. read_image). Both message converters emitted the image carrier as a user message immediately after each tool result, so a two-call turn went assistant -> tool -> user(image) -> tool -> user(image). The Command Code gateway validates that an assistant's tool blocks are answered consecutively, so the interleaved user message made the whole request fail with 'An assistant message with tool_calls must be followed by tool messages responding to each tool_call_id' (HTTP 400) on every replay of the history. Buffer the image carriers in a pending list and flush them only once the whole tool group has been emitted (or the next non-tool message is reached), for both the CLI (messagesToCC) and OpenAI (messagesToOpenAI) transports. Adds regression tests pinning the assistant -> tool -> tool -> user -> user ordering.
Mars-Sea
added a commit
that referenced
this pull request
Sep 11, 2026
Mars-Sea
added a commit
that referenced
this pull request
Sep 12, 2026
A turn's tool-result image carriers travel as a group after the whole tool group (issue #33), so nothing but position tied an image back to its own result. Every read_image result renders the same envelope text, so two parallel calls produced two identical notes and the model had to infer the pairing from order alone. The note now names the call: 'Attached image(s) from tool result (call-a):'. It uses the WIRE id — the one the model saw on the tool-call it issued and on the tool message just above — so an overlong cross-provider id is named by the short alias that replaced it (issue #23), not by the harness-side id that appears nowhere in the request. Both converters now resolve that id once and use it for the tool message and the note. Also rebuilds lib/, which incidentally normalizes the CRLF line endings a CRLF checkout baked into both committed sourcemaps' sourcesContent. Tests: the two parallel-carrier tests now assert each note names its own call, plus a new test pinning that a remapped overlong id is named by its alias rather than the harness id. Each fails against the previous adapter.
Mars-Sea
added a commit
that referenced
this pull request
Sep 12, 2026
The two converters disagreed about the same input: the CLI transport emitted
`{ role: 'user', content: [] }` for a user message with no text and no image
to put on the wire, while the Provider API transport skipped it. An empty
message carries nothing to the model, and an empty content array is a needless
gateway-compat risk, so both transports now skip it. (dsh-llm-deepseek pushes
`content: ''` instead; skipping is the safer half of that divergence to keep.)
The flush of pending tool-result image carriers runs before the skip on both
transports, so an image queued behind an empty message is still emitted rather
than dropped with it — pinned by the new test.
Also adds the gateway's tool-group rule as an explicit, reusable invariant
instead of pinning shapes one at a time. One table of ten representative
histories now runs through both transports and asserts, from the emitted wire
body, that every assistant message's tool calls are answered consecutively
with nothing interleaved. It re-derives the rule from the output rather than
the input, so it holds for both wire shapes and survives a converter
refactor. Verified to fail against the pre-#33 adapter with the expected
diagnostic, and the invariant carries its own test so it cannot pass
vacuously.
Note: tests/ is outside the tsconfig include list, so `npm run typecheck`
never checked this file — an unimported `ContentBlock` (pre-existing, plus one
this commit introduced) went unnoticed. Both imports are now declared.
Mars-Sea
added a commit
that referenced
this pull request
Sep 12, 2026
- Thinking models no longer fail every turn after the first tool call. The /alpha/generate transport had stopped replaying historical reasoning, but the provider rebuilds its request from those parts and rejects an assistant tool call that arrives without them, so a Go-plan or fallback session was unusable with a thinking model. Reasoning is replayed in content order again, on both transports (issue #34). - A tool whose parameter schema has no object root no longer kills the whole turn. The gateway validates every function schema root and refuses the request otherwise, so one badly declared third-party or MCP tool made every model and every tool fail. Both bodies now normalize the root: an object-rooted schema is untouched, a type-less object-shaped one gains the type, a union is collapsed, a root $ref is inlined, and a non-object root degrades to a permissive object (issue #35). - Reading several images in one turn no longer makes the conversation unusable. Image carriers are buffered until the whole tool group has been emitted, so parallel tool results stay adjacent, and each carrier names the tool call it came from (issue #33). - A user message that converts to nothing is dropped by both transports instead of being sent as an empty content array. - npm run typecheck now checks the test suite too; no shipped behavior change. - Synced with command-code@1.53.1. The catalog, effort map, plan tiers, subscription maps, deals, peak/off-peak membership and schedule, and the wire format are all unchanged; only the request-header version moved. gpt-5.6-terra/luna are now routed upstream through vercel-ai-gateway (same efforts, modalities and context), which is invisible to this plugin.
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.
What
Both message converters emitted a tool-result image carrier as a
usermessage immediately after its own tool result. When one assistant turn contains several parallel tool calls whose results carry images (e.g. tworead_imagecalls), the request came out as:The Command Code gateway validates that an assistant message's tool blocks are answered consecutively, so the interleaved user message rejected the whole request:
This fires on every history replay of such a session, so the conversation cannot continue at all (a retry does not help; only switching provider/model works).
Fix
Buffer the carries in a
pendingImageslist and flush them only after the whole tool group has been emitted - right before the next non-tool message (user or assistant), or at the end of the loop. Both transports are fixed:messagesToCC(CLI/alpha/generate)messagesToOpenAI(Provider API/provider/v1/chat/completions)Result:
Single-call turns are unchanged (
tool -> user(image)), so the issue #30 shape is preserved.Tests
Two regression tests pin the ordering on both transports (
stream() keeps parallel tool results consecutive before their image carriers,openai protocol groups carried images after the whole parallel tool turn).npm run typecheckand the fullnode --import tsx --test tests/*.test.tssuite (91 tests) pass.lib/was rebuilt withnpm run buildand staged withgit add -f.Repro notes
Observed with a DeepSeek Harness session where one assistant turn issued two parallel
read_imagecalls; every later turn oncommandcodefailed with that 400 until the offending history was left behind.