From a9214680e86a222a8c6f558f437fdcb75a11ee0d Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 2 Feb 2026 15:19:29 -0600 Subject: [PATCH 1/2] Revert "fix: adjust resolve parts so that when messages with multiple @ references occur, the tool calls are properly ordered" This reverts commit 612b656d3670f252541be79f96bfda31d78dcf73. --- packages/opencode/src/session/processor.ts | 10 +--- packages/opencode/src/session/prompt.ts | 69 ++++++++-------------- packages/opencode/src/tool/batch.ts | 8 +-- packages/opencode/src/tool/read.ts | 4 ++ packages/opencode/src/tool/tool.ts | 2 +- 5 files changed, 33 insertions(+), 60 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 24b4a4f9fbc8..b5289e903a16 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -172,14 +172,6 @@ export namespace SessionProcessor { case "tool-result": { const match = toolcalls[value.toolCallId] if (match && match.state.status === "running") { - const attachments = value.output.attachments?.map( - (attachment: Omit) => ({ - ...attachment, - id: Identifier.ascending("part"), - messageID: match.messageID, - sessionID: match.sessionID, - }), - ) await Session.updatePart({ ...match, state: { @@ -192,7 +184,7 @@ export namespace SessionProcessor { start: match.state.time.start, end: Date.now(), }, - attachments, + attachments: value.output.attachments, }, }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 98dce97ba90d..e0861c4df527 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -187,17 +187,13 @@ export namespace SessionPrompt { text: template, }, ] - const matches = ConfigMarkdown.files(template) + const files = ConfigMarkdown.files(template) const seen = new Set() - const names = matches - .map((match) => match[1]) - .filter((name) => { - if (seen.has(name)) return false + await Promise.all( + files.map(async (match) => { + const name = match[1] + if (seen.has(name)) return seen.add(name) - return true - }) - const resolved = await Promise.all( - names.map(async (name) => { const filepath = name.startsWith("~/") ? path.join(os.homedir(), name.slice(2)) : path.resolve(Instance.worktree, name) @@ -205,34 +201,33 @@ export namespace SessionPrompt { const stats = await fs.stat(filepath).catch(() => undefined) if (!stats) { const agent = await Agent.get(name) - if (!agent) return undefined - return { - type: "agent", - name: agent.name, - } satisfies PromptInput["parts"][number] + if (agent) { + parts.push({ + type: "agent", + name: agent.name, + }) + } + return } if (stats.isDirectory()) { - return { + parts.push({ type: "file", url: `file://${filepath}`, filename: name, mime: "application/x-directory", - } satisfies PromptInput["parts"][number] + }) + return } - return { + parts.push({ type: "file", url: `file://${filepath}`, filename: name, mime: "text/plain", - } satisfies PromptInput["parts"][number] + }) }), ) - for (const item of resolved) { - if (!item) continue - parts.push(item) - } return parts } @@ -432,12 +427,6 @@ export namespace SessionPrompt { assistantMessage.time.completed = Date.now() await Session.updateMessage(assistantMessage) if (result && part.state.status === "running") { - const attachments = result.attachments?.map((attachment) => ({ - ...attachment, - id: Identifier.ascending("part"), - messageID: assistantMessage.id, - sessionID: assistantMessage.sessionID, - })) await Session.updatePart({ ...part, state: { @@ -446,7 +435,7 @@ export namespace SessionPrompt { title: result.title, metadata: result.metadata, output: result.output, - attachments, + attachments: result.attachments, time: { ...part.state.time, end: Date.now(), @@ -785,13 +774,16 @@ export namespace SessionPrompt { ) const textParts: string[] = [] - const attachments: Omit[] = [] + const attachments: MessageV2.FilePart[] = [] for (const contentItem of result.content) { if (contentItem.type === "text") { textParts.push(contentItem.text) } else if (contentItem.type === "image") { attachments.push({ + id: Identifier.ascending("part"), + sessionID: input.session.id, + messageID: input.processor.message.id, type: "file", mime: contentItem.mimeType, url: `data:${contentItem.mimeType};base64,${contentItem.data}`, @@ -803,6 +795,9 @@ export namespace SessionPrompt { } if (resource.blob) { attachments.push({ + id: Identifier.ascending("part"), + sessionID: input.session.id, + messageID: input.processor.message.id, type: "file", mime: resource.mimeType ?? "application/octet-stream", url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`, @@ -1051,7 +1046,6 @@ export namespace SessionPrompt { pieces.push( ...result.attachments.map((attachment) => ({ ...attachment, - id: Identifier.ascending("part"), synthetic: true, filename: attachment.filename ?? part.filename, messageID: info.id, @@ -1189,18 +1183,7 @@ export namespace SessionPrompt { }, ] }), - ) - .then((x) => x.flat()) - .then((drafts) => - drafts.map( - (part): MessageV2.Part => ({ - ...part, - id: Identifier.ascending("part"), - messageID: info.id, - sessionID: input.sessionID, - }), - ), - ) + ).then((x) => x.flat()) await Plugin.trigger( "chat.message", diff --git a/packages/opencode/src/tool/batch.ts b/packages/opencode/src/tool/batch.ts index b5c3ad0a12b6..ba34eb48f5cf 100644 --- a/packages/opencode/src/tool/batch.ts +++ b/packages/opencode/src/tool/batch.ts @@ -77,12 +77,6 @@ export const BatchTool = Tool.define("batch", async () => { }) const result = await tool.execute(validatedParams, { ...ctx, callID: partID }) - const attachments = result.attachments?.map((attachment) => ({ - ...attachment, - id: Identifier.ascending("part"), - messageID: ctx.messageID, - sessionID: ctx.sessionID, - })) await Session.updatePart({ id: partID, @@ -97,7 +91,7 @@ export const BatchTool = Tool.define("batch", async () => { output: result.output, title: result.title, metadata: result.metadata, - attachments, + attachments: result.attachments, time: { start: callStartTime, end: Date.now(), diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 13236d44dd46..f230cdf44cbb 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -6,6 +6,7 @@ import { LSP } from "../lsp" import { FileTime } from "../file/time" import DESCRIPTION from "./read.txt" import { Instance } from "../project/instance" +import { Identifier } from "../id/id" import { assertExternalDirectory } from "./external-directory" import { InstructionPrompt } from "../session/instruction" @@ -78,6 +79,9 @@ export const ReadTool = Tool.define("read", { }, attachments: [ { + id: Identifier.ascending("part"), + sessionID: ctx.sessionID, + messageID: ctx.messageID, type: "file", mime, url: `data:${mime};base64,${Buffer.from(await file.bytes()).toString("base64")}`, diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 0e78ba665cfc..3d17ea192d32 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -36,7 +36,7 @@ export namespace Tool { title: string metadata: M output: string - attachments?: Omit[] + attachments?: MessageV2.FilePart[] }> formatValidationError?(error: z.ZodError): string }> From 0e74e6d0dcf00020e8c7fdb4fe3a2e6a2b71a7b6 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 2 Feb 2026 15:24:19 -0600 Subject: [PATCH 2/2] Revert "test: add unit test" This reverts commit 5db089070a24d66063f55d4f5baf0da20883daf9. --- packages/opencode/test/session/prompt.test.ts | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 packages/opencode/test/session/prompt.test.ts diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts deleted file mode 100644 index e778bfe5146a..000000000000 --- a/packages/opencode/test/session/prompt.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import path from "path" -import { describe, expect, test } from "bun:test" -import { Session } from "../../src/session" -import { SessionPrompt } from "../../src/session/prompt" -import { MessageV2 } from "../../src/session/message-v2" -import { Instance } from "../../src/project/instance" -import { Log } from "../../src/util/log" -import { tmpdir } from "../fixture/fixture" - -Log.init({ print: false }) - -describe("SessionPrompt ordering", () => { - test("keeps @file order with read output parts", async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - await Bun.write(path.join(dir, "a.txt"), "28\n") - await Bun.write(path.join(dir, "b.txt"), "42\n") - }, - }) - - await Instance.provide({ - directory: tmp.path, - fn: async () => { - const session = await Session.create({}) - const template = "What numbers are written in files @a.txt and @b.txt ?" - const parts = await SessionPrompt.resolvePromptParts(template) - const fileParts = parts.filter((part) => part.type === "file") - - expect(fileParts.map((part) => part.filename)).toStrictEqual(["a.txt", "b.txt"]) - - const message = await SessionPrompt.prompt({ - sessionID: session.id, - parts, - noReply: true, - }) - const stored = await MessageV2.get({ sessionID: session.id, messageID: message.info.id }) - const items = stored.parts - const aPath = path.join(tmp.path, "a.txt") - const bPath = path.join(tmp.path, "b.txt") - const sequence = items.flatMap((part) => { - if (part.type === "text") { - if (part.text.includes(aPath)) return ["input:a"] - if (part.text.includes(bPath)) return ["input:b"] - if (part.text.includes("00001| 28")) return ["output:a"] - if (part.text.includes("00001| 42")) return ["output:b"] - return [] - } - if (part.type === "file") { - if (part.filename === "a.txt") return ["file:a"] - if (part.filename === "b.txt") return ["file:b"] - } - return [] - }) - - expect(sequence).toStrictEqual(["input:a", "output:a", "file:a", "input:b", "output:b", "file:b"]) - - await Session.remove(session.id) - }, - }) - }) -})