From fd6f7133c556f1b4f2cc9769cc18ddae61ab8de7 Mon Sep 17 00:00:00 2001 From: Ryan Skidmore Date: Mon, 2 Mar 2026 21:52:43 -0600 Subject: [PATCH] fix(opencode): clone part data in Bus event to preserve token values (#15780) --- packages/opencode/src/session/index.ts | 2 +- .../opencode/test/session/session.test.ts | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index e8db405fd..b11763205 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -761,7 +761,7 @@ export namespace Session { .run() Database.effect(() => Bus.publish(MessageV2.Event.PartUpdated, { - part, + part: structuredClone(part), }), ) }) diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 219cef127..2de94ee7e 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -4,6 +4,8 @@ import { Session } from "../../src/session" import { Bus } from "../../src/bus" import { Log } from "../../src/util/log" import { Instance } from "../../src/project/instance" +import { MessageV2 } from "../../src/session/message-v2" +import { Identifier } from "../../src/id/id" const projectRoot = path.join(__dirname, "../..") Log.init({ print: false }) @@ -69,3 +71,68 @@ describe("session.started event", () => { }) }) }) + +describe("step-finish token propagation via Bus event", () => { + test("non-zero tokens propagate through PartUpdated event", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const session = await Session.create({}) + + const messageID = Identifier.ascending("message") + await Session.updateMessage({ + id: messageID, + sessionID: session.id, + role: "user", + time: { created: Date.now() }, + agent: "user", + model: { providerID: "test", modelID: "test" }, + tools: {}, + mode: "", + } as unknown as MessageV2.Info) + + let received: MessageV2.Part | undefined + const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, (event) => { + received = event.properties.part + }) + + const tokens = { + total: 1500, + input: 500, + output: 800, + reasoning: 200, + cache: { read: 100, write: 50 }, + } + + const partInput = { + id: Identifier.ascending("part"), + messageID, + sessionID: session.id, + type: "step-finish" as const, + reason: "stop", + cost: 0.005, + tokens, + } + + await Session.updatePart(partInput) + + await new Promise((resolve) => setTimeout(resolve, 100)) + + expect(received).toBeDefined() + expect(received!.type).toBe("step-finish") + const finish = received as MessageV2.StepFinishPart + expect(finish.tokens.input).toBe(500) + expect(finish.tokens.output).toBe(800) + expect(finish.tokens.reasoning).toBe(200) + expect(finish.tokens.total).toBe(1500) + expect(finish.tokens.cache.read).toBe(100) + expect(finish.tokens.cache.write).toBe(50) + expect(finish.cost).toBe(0.005) + expect(received).not.toBe(partInput) + + unsub() + await Session.remove(session.id) + }, + }) + }, { timeout: 30000 }) +})