From 8830d2a8ddf51c1ae044fa76f60881c1901469d2 Mon Sep 17 00:00:00 2001 From: Shoaib Akhtar Ansari Date: Fri, 16 Jan 2026 19:11:19 +0530 Subject: [PATCH 1/2] fix(session): safeguard message conversion against invalid prompts (#8862) --- packages/opencode/src/session/message-v2.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 4b081b5b44e..5e8326ac330 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -517,8 +517,11 @@ export namespace MessageV2 { input: part.state.input, output: part.state.time.compacted ? "[Old tool result content cleared]" - : { output: part.state.output, attachments: part.state.attachments }, - callProviderMetadata: part.metadata, + : { + output: part.state.output, + attachments: part.state.attachments?.filter((x) => x) ?? [], + }, + callProviderMetadata: part.metadata ?? {}, }) } if (part.state.status === "error") @@ -556,12 +559,12 @@ export namespace MessageV2 { } } - return convertToModelMessages( - result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")), - { - tools: options?.tools, - }, - ) + const filtered = result + .filter((msg) => msg.parts.length > 0) + .filter((msg) => msg.parts.some((part) => part.type !== "step-start")) + return convertToModelMessages(filtered, { + tools: options?.tools, + }) } export const stream = fn(Identifier.schema("session"), async function* (sessionID) { From deac3fd3db05f4f8cd0a734a96f11b62ad2e9153 Mon Sep 17 00:00:00 2001 From: Shoaib Akhtar Ansari Date: Sat, 17 Jan 2026 13:24:40 +0530 Subject: [PATCH 2/2] fix(session): combine filters and add regression test --- packages/opencode/src/session/message-v2.ts | 6 +- .../opencode/test/session/issue-8862.test.ts | 99 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 packages/opencode/test/session/issue-8862.test.ts diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 5e8326ac330..fbdfd1bc477 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -559,9 +559,9 @@ export namespace MessageV2 { } } - const filtered = result - .filter((msg) => msg.parts.length > 0) - .filter((msg) => msg.parts.some((part) => part.type !== "step-start")) + const filtered = result.filter( + (msg) => msg.parts.length > 0 && msg.parts.some((part) => part.type !== "step-start"), + ) return convertToModelMessages(filtered, { tools: options?.tools, }) diff --git a/packages/opencode/test/session/issue-8862.test.ts b/packages/opencode/test/session/issue-8862.test.ts new file mode 100644 index 00000000000..f5e7d876c1c --- /dev/null +++ b/packages/opencode/test/session/issue-8862.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, test } from "bun:test" +import { MessageV2 } from "../../src/session/message-v2" + +const sessionID = "session" + +function assistantInfo(id: string, parentID: string): MessageV2.Assistant { + return { + id, + sessionID, + role: "assistant", + time: { created: 0 }, + parentID, + modelID: "model", + providerID: "provider", + mode: "", + agent: "agent", + path: { cwd: "/", root: "/" }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + } as unknown as MessageV2.Assistant +} + +function basePart(messageID: string, id: string) { + return { + id, + sessionID, + messageID, + } +} + +describe("issue-8862 regression test", () => { + test("successfully converts tool results with undefined metadata/attachments", () => { + const userID = "m-user" + const assistantID = "m-assistant" + + const input: MessageV2.WithParts[] = [ + { + info: assistantInfo(assistantID, userID), + parts: [ + { + ...basePart(assistantID, "a2"), + type: "tool", + callID: "call-1", + tool: "bash", + state: { + status: "completed", + input: { cmd: "ls" }, + output: "ok", + title: "Bash", + metadata: undefined as any, + time: { start: 0, end: 1 }, + attachments: undefined as any, + } as any, + metadata: undefined as any, + }, + ] as MessageV2.Part[], + }, + ] + + expect(MessageV2.toModelMessage(input)).toStrictEqual([ + { + role: "assistant", + content: [ + { + type: "tool-call", + toolCallId: "call-1", + toolName: "bash", + input: { cmd: "ls" }, + providerExecuted: undefined, + providerOptions: {}, + }, + ], + }, + { + role: "tool", + content: [ + { + type: "tool-result", + toolCallId: "call-1", + toolName: "bash", + output: { + type: "json", + value: { + output: "ok", + attachments: [], + }, + }, + providerOptions: {}, + }, + ], + }, + ]) + }) +})