From a863bc0c8a4c7eb3112fcfeedeefa6a102008061 Mon Sep 17 00:00:00 2001 From: Idris Gadi Date: Sat, 10 Jan 2026 23:03:12 +0530 Subject: [PATCH 1/7] feat(opencode): add cost tracking to the session information --- packages/opencode/src/session/index.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index a204913f77d..b80d21f9b75 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -57,6 +57,7 @@ export namespace Session { .optional(), title: z.string(), version: z.string(), + cost: z.number().optional(), time: z.object({ created: z.number(), updated: z.number(), @@ -147,9 +148,13 @@ export namespace Session { messageID: Identifier.schema("message").optional(), }), async (input) => { + const parentCost = await getCost(input.sessionID) const session = await createNext({ directory: Instance.directory, }) + await update(session.id, (draft) => { + draft.cost = parentCost + }) const msgs = await messages({ sessionID: input.sessionID }) const idMap = new Map() @@ -199,6 +204,7 @@ export namespace Session { directory: input.directory, parentID: input.parentID, title: input.title ?? createDefaultTitle(!!input.parentID), + cost: 0, permission: input.permission, time: { created: Date.now(), @@ -236,6 +242,11 @@ export namespace Session { return Storage.read(["share", id]) }) + export const getCost = fn(Identifier.schema("session"), async (id) => { + const read = await Storage.read(["session", Instance.project.id, id]) + return read.cost ?? 0 + }) + export const share = fn(Identifier.schema("session"), async (id) => { const cfg = await Config.get() if (cfg.share === "disabled") { @@ -272,6 +283,13 @@ export namespace Session { return result } + export async function addCost(sessionID: string, amount: number) { + if (amount === 0) return + await update(sessionID, (draft) => { + draft.cost = (draft.cost ?? 0) + amount + }) + } + export const diff = fn(Identifier.schema("session"), async (sessionID) => { const diffs = await Storage.read(["session_diff", sessionID]) return diffs ?? [] From ba86463841a11505ad1e434ef04fc50536156d13 Mon Sep 17 00:00:00 2001 From: Idris Gadi Date: Sat, 10 Jan 2026 23:05:41 +0530 Subject: [PATCH 2/7] fix(tui): read actual cost from the session informatin instead of deriving from the messages array max 100 messages are kept in the store/memory which leads to misreporting of the actual spend value --- packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx | 2 +- packages/opencode/src/session/processor.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index c0f4bd74abd..d6b4b601ada 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -41,7 +41,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ) const cost = createMemo(() => { - const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) + const total = session().cost ?? 0 return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 71db7f13677..3191c521bb3 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -253,6 +253,7 @@ export namespace SessionProcessor { cost: usage.cost, }) await Session.updateMessage(input.assistantMessage) + await Session.addCost(input.sessionID, usage.cost) if (snapshot) { const patch = await Snapshot.patch(snapshot) if (patch.files.length) { From 9ca6a618e6e2935f1c1abb9fe354cb88ccb54186 Mon Sep 17 00:00:00 2001 From: Idris Gadi Date: Sat, 10 Jan 2026 23:49:54 +0530 Subject: [PATCH 3/7] feat: add migration step to populate old session with cost --- packages/opencode/src/storage/storage.ts | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 8b4042ea13f..38c302ba416 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -138,6 +138,58 @@ export namespace Storage { ) } }, + async (dir) => { + log.info("migrating session costs") + const startTime = Date.now() + let migratedCount = 0 + + for await (const sessionPath of new Bun.Glob("session/*/*.json").scan({ + cwd: dir, + absolute: true, + })) { + const session = await Bun.file(sessionPath).json() + if (!session) continue + if (session.cost != null) continue + + let totalCost = 0 + const messageDir = path.join(dir, "message", session.id) + + if (await fs.exists(messageDir)) { + for await (const messagePath of new Bun.Glob("*.json").scan({ + cwd: messageDir, + absolute: true, + })) { + const message = await Bun.file(messagePath).json() + if (message.role !== "assistant") continue + totalCost += message.cost ?? 0 + } + } + + await Bun.file(sessionPath).write( + JSON.stringify( + { + ...session, + cost: totalCost, + }, + null, + 2, + ), + ) + + migratedCount++ + if (migratedCount % 50 === 0) { + log.info("cost migration progress", { + sessionsMigrated: migratedCount, + elapsedMs: Date.now() - startTime, + }) + } + } + + log.info("cost migration complete", { + sessionsMigrated: migratedCount, + elapsedMs: Date.now() - startTime, + }) + }, ] const state = lazy(async () => { From 97ee2529a610f069bdf71ccf5382b78087e1470f Mon Sep 17 00:00:00 2001 From: Idris Gadi Date: Sun, 11 Jan 2026 14:11:15 +0530 Subject: [PATCH 4/7] fix: forked sessions should start with $0 spent value --- packages/opencode/src/session/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index b80d21f9b75..a42efdf87da 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -148,13 +148,9 @@ export namespace Session { messageID: Identifier.schema("message").optional(), }), async (input) => { - const parentCost = await getCost(input.sessionID) const session = await createNext({ directory: Instance.directory, }) - await update(session.id, (draft) => { - draft.cost = parentCost - }) const msgs = await messages({ sessionID: input.sessionID }) const idMap = new Map() From d6dbb1db8e722d96b863b3f781063f927a85f73f Mon Sep 17 00:00:00 2001 From: Idris Gadi Date: Thu, 15 Jan 2026 07:15:40 +0530 Subject: [PATCH 5/7] fix: return undefined instead of 0 if cost is not found this informs the caller function that cost can be undefined so it can properly handle it, instead of just defaulting to 0. --- packages/opencode/src/session/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index a42efdf87da..5b166a1eea1 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -240,7 +240,7 @@ export namespace Session { export const getCost = fn(Identifier.schema("session"), async (id) => { const read = await Storage.read(["session", Instance.project.id, id]) - return read.cost ?? 0 + return read.cost }) export const share = fn(Identifier.schema("session"), async (id) => { From 96212fa3963ed3c976f613fef74eeafbcfc6492a Mon Sep 17 00:00:00 2001 From: Idris Gadi Date: Thu, 15 Jan 2026 07:19:52 +0530 Subject: [PATCH 6/7] chore: remove log I added for debugging as it doesn't match the projects style --- packages/opencode/src/storage/storage.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 38c302ba416..5dc9d2b430d 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -177,12 +177,6 @@ export namespace Storage { ) migratedCount++ - if (migratedCount % 50 === 0) { - log.info("cost migration progress", { - sessionsMigrated: migratedCount, - elapsedMs: Date.now() - startTime, - }) - } } log.info("cost migration complete", { From affc11a7ada22329b4ceeaf337490d22d8d77209 Mon Sep 17 00:00:00 2001 From: Idris Gadi Date: Thu, 15 Jan 2026 07:24:41 +0530 Subject: [PATCH 7/7] feat: fallback to deriving cost from messages rather than defaulting to 0 --- packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index d6b4b601ada..30cc50bfab1 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -41,7 +41,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ) const cost = createMemo(() => { - const total = session().cost ?? 0 + const total = session().cost ?? messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD",