Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/transforms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,38 @@ describe("transforms", () => {
assert.equal(parsed.messages[0].content[1].text, "hello")
})

it("transformBody preserves leading tool_result blocks when relocating system text", () => {
const input = JSON.stringify({
system: [{ type: "text", text: "summarize this session" }],
messages: [
{
role: "assistant",
content: [
{ type: "tool_use", id: "toolu_1", name: "bash", input: {} },
],
},
{
role: "user",
content: [
{ type: "tool_result", tool_use_id: "toolu_1", content: "ok" },
],
},
],
})

const output = transformBody(input)
const parsed = JSON.parse(output as string) as {
messages: Array<{
content: Array<{ type: string; text?: string; tool_use_id?: string }>
}>
}

assert.equal(parsed.messages[1].content[0].type, "tool_result")
assert.equal(parsed.messages[1].content[0].tool_use_id, "toolu_1")
assert.equal(parsed.messages[1].content[1].type, "text")
assert.equal(parsed.messages[1].content[1].text, "summarize this session")
})

it("transformBody keeps system intact when no messages exist", () => {
const input = JSON.stringify({
system: [{ type: "text", text: "Some instructions" }],
Expand Down Expand Up @@ -821,6 +853,50 @@ describe("transforms", () => {
)
})

it("transformBody appends a user turn when messages end with assistant", () => {
const input = JSON.stringify({
model: "claude-opus-4-7",
messages: [
{ role: "user", content: "summarize the session" },
{ role: "assistant", content: "Here is the summary." },
],
})

const output = transformBody(input)
const parsed = JSON.parse(output as string) as {
messages: Array<{ role: string; content: unknown }>
}

assert.equal(parsed.messages.length, 3)
assert.equal(parsed.messages[1].role, "assistant")
assert.equal(parsed.messages[2].role, "user")
assert.deepEqual(parsed.messages[2].content, [
{
type: "text",
text: "Please pause and wait for further instructions.",
},
])
})

it("transformBody appends a user turn for any model ending with assistant", () => {
const input = JSON.stringify({
model: "claude-sonnet-4-5",
messages: [
{ role: "user", content: "summarize the session" },
{ role: "assistant", content: "Here is the summary." },
],
})

const output = transformBody(input)
const parsed = JSON.parse(output as string) as {
messages: Array<{ role: string; content: unknown }>
}

assert.equal(parsed.messages.length, 3)
assert.equal(parsed.messages[1].role, "assistant")
assert.equal(parsed.messages[2].role, "user")
})
Comment on lines +881 to +898

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Second trailing-assistant test omits content assertion

The Opus 4.7 test (above) verifies the exact shape of the appended content block with deepEqual, but this model-agnostic variant only checks messages.length and messages[2].role. Adding a deepEqual on messages[2].content here would confirm the injected text is consistent across models and guard against a future regression where the content accidentally diverges per model.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/transforms.test.ts
Line: 881-898

Comment:
**Second trailing-assistant test omits content assertion**

The Opus 4.7 test (above) verifies the exact shape of the appended content block with `deepEqual`, but this model-agnostic variant only checks `messages.length` and `messages[2].role`. Adding a `deepEqual` on `messages[2].content` here would confirm the injected text is consistent across models and guard against a future regression where the content accidentally diverges per model.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor Fix in Claude Code Fix in Codex


it("transformResponseStream flushes remaining buffered data on stream end", async () => {
const encoder = new TextEncoder()
const chunk1 = 'data: {"name":"mcp_alpha"}\n\n'
Expand Down
25 changes: 24 additions & 1 deletion src/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,17 @@ export function transformBody(
if (typeof firstUser.content === "string") {
firstUser.content = prefix + "\n\n" + firstUser.content
} else if (Array.isArray(firstUser.content)) {
firstUser.content.unshift({ type: "text", text: prefix })
const insertAt = firstUser.content.findIndex(
(block) => block.type !== "tool_result",
)
firstUser.content.splice(
insertAt === -1 ? firstUser.content.length : insertAt,
0,
{
type: "text",
text: prefix,
},
)
}
}
}
Expand Down Expand Up @@ -253,6 +263,19 @@ export function transformBody(

if (Array.isArray(parsed.messages)) {
parsed.messages = repairToolPairs(parsed.messages)

const lastMessage = parsed.messages.at(-1)
if (lastMessage?.role === "assistant") {
parsed.messages.push({
role: "user",
content: [
{
type: "text",
text: "Please pause and wait for further instructions.",
},
],
})
}
Comment on lines +267 to +278

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Trailing-assistant guard does not check for active tool_use blocks

After repairToolPairs, a last assistant message can still contain tool_use blocks if those blocks are considered non-orphaned (i.e. there happens to be a matching tool_result earlier in the (already broken) conversation). In that case the code appends a plain-text user turn, producing a conversation where the tool_use call is never answered by a tool_result — a structure the API rejects with a different error than the one this PR targets. Adding a check that the last assistant message contains no tool_use content before appending the synthetic turn would make the guard more defensive against unusual compaction outputs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/transforms.ts
Line: 267-278

Comment:
**Trailing-assistant guard does not check for active `tool_use` blocks**

After `repairToolPairs`, a last assistant message can still contain `tool_use` blocks if those blocks are considered non-orphaned (i.e. there happens to be a matching `tool_result` earlier in the (already broken) conversation). In that case the code appends a plain-text user turn, producing a conversation where the `tool_use` call is never answered by a `tool_result` — a structure the API rejects with a different error than the one this PR targets. Adding a check that the last assistant message contains no `tool_use` content before appending the synthetic turn would make the guard more defensive against unusual compaction outputs.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

}

return JSON.stringify(parsed)
Expand Down