From 889946c00f23d8a166ffab84fd1c0b757b409191 Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:45:39 +0300 Subject: [PATCH 1/2] fix(opencode): read usage from each instance data directory --- .../src/server/usage.test.ts | 168 +++++++++++++++++- .../provider-opencode/src/server/usage.ts | 12 +- 2 files changed, 175 insertions(+), 5 deletions(-) diff --git a/packages/provider-opencode/src/server/usage.test.ts b/packages/provider-opencode/src/server/usage.test.ts index 4dc7ec99486c..697ac2854454 100644 --- a/packages/provider-opencode/src/server/usage.test.ts +++ b/packages/provider-opencode/src/server/usage.test.ts @@ -1,5 +1,12 @@ +// @effect-diagnostics nodeBuiltinImport:off - directory aliases use junctions on Windows, +// which Effect FileSystem.symlink does not expose. +import * as NodeFSP from "node:fs/promises"; + import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; +import { DEFAULT_SERVER_SETTINGS, ProviderInstanceId } from "@t3tools/contracts"; +import type { ProviderUsageInstance } from "@t3tools/provider-core/server/usage"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -9,7 +16,8 @@ import * as Path from "effect/Path"; import * as SqlClient from "effect/sql/SqlClient"; import { vi } from "vite-plus/test"; -import { readOpenCodeUsage } from "./usage.ts"; +import type { OpenCodeSettings } from "../settings.ts"; +import { openCodeUsageReader, readOpenCodeUsage } from "./usage.ts"; /** A writable connection that stays open until the test's scope closes. */ const openDatabase = (filename: string) => @@ -17,6 +25,42 @@ const openDatabase = (filename: string) => Effect.map(Context.get(SqlClient.SqlClient)), ); +const seedHistory = Effect.fn("seedHistory")(function* (root: string, id: string) { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fileSystem.makeDirectory(root, { recursive: true }); + const db = yield* openDatabase(path.join(root, "opencode.db")); + yield* db.unsafe("CREATE TABLE message (id TEXT, session_id TEXT, data TEXT)"); + yield* db.unsafe("INSERT INTO message VALUES (?, ?, ?)", [ + id, + "session-1", + JSON.stringify({ + role: "assistant", + modelID: "claude-sonnet-4-5", + time: { created: 1780000000000 }, + tokens: { input: 100, output: 20 }, + }), + ]); +}); + +const usageInstance = (id: string, environment: NodeJS.ProcessEnv, configured = true) => ({ + instanceId: ProviderInstanceId.make(id), + config: undefined, + environment, + configured, +}); + +const scanUsage = (instances: readonly ProviderUsageInstance[]) => { + if (openCodeUsageReader.kind !== "scan") throw new Error("Expected an OpenCode scan reader"); + return openCodeUsageReader.scan({ + instances, + settings: DEFAULT_SERVER_SETTINGS, + windowStartMs: 0, + retentionCutoffMs: 0, + awaitRefresh: true, + }); +}; + describe("readOpenCodeUsage", () => { it.effect("bounds raw history allocations while counting every eligible message once", () => Effect.gen(function* () { @@ -274,3 +318,125 @@ describe("readOpenCodeUsage", () => { ); }); import * as NodeSqlite from "node:sqlite"; + +describe("openCodeUsageReader", () => { + it.effect("reads histories from each instance's environment", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const temp = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-instances-test-" }); + const home = yield* fileSystem.realPath(temp); + const first = path.join(home, "first"); + const dataHome = path.join(home, "second"); + const second = path.join(dataHome, "opencode"); + yield* seedHistory(first, "msg-first"); + yield* seedHistory(second, "msg-second"); + + const sources = yield* scanUsage([ + usageInstance("opencode-first", { + OPENCODE_DATA_DIR: " ~/first, , ", + XDG_DATA_HOME: path.join(home, "unused"), + }), + usageInstance("opencode-second", { XDG_DATA_HOME: ` ${dataHome} ` }), + ]).pipe( + Effect.provideService(HostProcess.Environment, { + OPENCODE_DATA_DIR: path.join(home, "host"), + }), + Effect.provideService(HostProcess.HomeDirectory, home), + ); + + assert.deepStrictEqual( + sources.map((source) => source.dir), + [first, second], + ); + assert.deepStrictEqual( + sources.map((source) => source.status), + ["ok", "ok"], + ); + const records = sources.flatMap( + (source) => source.files?.flatMap((file) => file.records) ?? [], + ); + assert.deepStrictEqual( + records.map((record) => record.dedupeKey), + ["opencode:msg-first", "opencode:msg-second"], + ); + assert.strictEqual( + records.reduce((sum, record) => sum + record.totals.uncachedInputTokens, 0), + 200, + ); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("scans canonical aliases shared by instances only once", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const temp = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-aliases-test-" }); + const home = yield* fileSystem.realPath(temp); + const root = path.join(home, "store"); + const alias = path.join(home, "alias"); + yield* seedHistory(root, "msg-shared"); + yield* Effect.tryPromise(() => NodeFSP.symlink(root, alias, "junction")); + const sources = yield* scanUsage([ + usageInstance("opencode-first", { OPENCODE_DATA_DIR: ` ${root}, ${alias} ` }), + usageInstance("opencode-second", { OPENCODE_DATA_DIR: alias }), + ]).pipe( + Effect.provideService(HostProcess.Environment, { + OPENCODE_DATA_DIR: path.join(home, "host"), + }), + ); + + assert.deepStrictEqual( + sources.map((source) => source.dir), + [root], + ); + assert.deepStrictEqual( + sources.flatMap( + (source) => + source.files?.flatMap((file) => file.records.map((record) => record.dedupeKey)) ?? [], + ), + ["opencode:msg-shared"], + ); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect.each([ + { dataHome: "absolute", dataDirs: undefined }, + { dataHome: "relative", dataDirs: " , " }, + { dataHome: "absent", dataDirs: undefined }, + ])("retains the implicit host default for $dataHome XDG_DATA_HOME", ({ dataHome, dataDirs }) => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const temp = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-default-test-" }); + const home = yield* fileSystem.realPath(temp); + const root = path.join(home, dataHome === "absolute" ? "xdg" : ".local/share", "opencode"); + const environment = { + OPENCODE_DATA_DIR: dataDirs, + XDG_DATA_HOME: + dataHome === "absolute" + ? path.join(home, "xdg") + : dataHome === "relative" + ? "relative" + : undefined, + }; + yield* seedHistory(root, "msg-default"); + + const sources = yield* scanUsage([usageInstance("opencode", environment, false)]).pipe( + Effect.provideService(HostProcess.Environment, environment), + Effect.provideService(HostProcess.HomeDirectory, home), + ); + assert.deepStrictEqual( + sources.map((source) => source.dir), + [root], + ); + assert.deepStrictEqual( + sources.flatMap( + (source) => + source.files?.flatMap((file) => file.records.map((record) => record.dedupeKey)) ?? [], + ), + ["opencode:msg-default"], + ); + }).pipe(Effect.provide(NodeServices.layer)), + ); +}); diff --git a/packages/provider-opencode/src/server/usage.ts b/packages/provider-opencode/src/server/usage.ts index 63b5568f0528..76815f28737c 100644 --- a/packages/provider-opencode/src/server/usage.ts +++ b/packages/provider-opencode/src/server/usage.ts @@ -321,10 +321,11 @@ export const readOpenCodeUsage = Effect.fn("readOpenCodeUsage")(function* ( * The data directories to read: `OPENCODE_DATA_DIR` (comma-separated) or the * XDG default, canonicalized so aliases count once. */ -const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* () { +const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* ( + environment: NodeJS.ProcessEnv, +) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const environment = yield* HostProcess.Environment; const homeDirectory = yield* HostProcess.HomeDirectory; const roots = environment["OPENCODE_DATA_DIR"] ?.split(",") @@ -352,8 +353,11 @@ export type OpenCodeUsageReaderEnv = FileSystem.FileSystem | Path.Path; export const openCodeUsageReader: ProviderUsageReader = { kind: "scan", provider: "opencode", - scan: Effect.fn("openCodeUsageReader.scan")(function* ({ windowStartMs }) { - const roots = yield* resolveOpenCodeDataDirs(); + scan: Effect.fn("openCodeUsageReader.scan")(function* ({ instances, windowStartMs }) { + const roots = new Set(); + for (const instance of instances) { + for (const root of yield* resolveOpenCodeDataDirs(instance.environment)) roots.add(root); + } return yield* Effect.forEach( roots, (dir) => From 2aea70dea8defa73506b40b7a5720b4e55517bbd Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:00:18 +0300 Subject: [PATCH 2/2] test(opencode): verify instance aliases in usage service fixture --- apps/server/src/usage/UsageService.test.ts | 70 ++++++++++++++++++- .../src/server/usage.test.ts | 37 ---------- 2 files changed, 69 insertions(+), 38 deletions(-) diff --git a/apps/server/src/usage/UsageService.test.ts b/apps/server/src/usage/UsageService.test.ts index af26c30a4770..107065fcbce3 100644 --- a/apps/server/src/usage/UsageService.test.ts +++ b/apps/server/src/usage/UsageService.test.ts @@ -9,8 +9,10 @@ import * as NodeSqlite from "node:sqlite"; import { assert, describe, it } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as HostProcess from "@t3tools/shared/HostProcess"; +import { OpenCodeDriver } from "@t3tools/provider-opencode/server"; import { mergeUsage } from "@t3tools/shared/usageMerge"; import { + DEFAULT_SERVER_SETTINGS, EnvironmentId, ProviderDriverKind, ProviderInstanceId, @@ -1019,6 +1021,17 @@ describe("UsageService", () => { yield* Effect.promise(async () => { await NodeFSP.mkdir(opencode); await NodeFSP.symlink(opencode, opencodeAlias, "junction"); + const messages = NodePath.join(opencode, "storage", "message", "session-1"); + await NodeFSP.mkdir(messages, { recursive: true }); + await NodeFSP.writeFile( + NodePath.join(messages, "msg-aliased.json"), + JSON.stringify({ + role: "assistant", + modelID: "example-model", + time: { created: Date.parse("2026-08-01T10:00:00Z") }, + tokens: { input: 100, output: 20 }, + }), + ); await NodeFSP.mkdir(conversations); await NodeFSP.mkdir(antigravityA); await NodeFSP.mkdir(antigravityB); @@ -1033,12 +1046,63 @@ describe("UsageService", () => { "junction", ); }); + const instances = [ + { + instanceId: ProviderInstanceId.make("opencode-first"), + config: undefined, + environment: { OPENCODE_DATA_DIR: ` ${opencode}, ${opencodeAlias} ` }, + configured: true, + }, + { + instanceId: ProviderInstanceId.make("opencode-second"), + config: undefined, + environment: { OPENCODE_DATA_DIR: opencodeAlias }, + configured: true, + }, + ]; + const reader = OpenCodeDriver.usage; + if (reader?.kind !== "scan") throw new Error("Expected an OpenCode scan reader"); + const scans = yield* reader + .scan({ + instances, + settings: DEFAULT_SERVER_SETTINGS, + windowStartMs: 0, + retentionCutoffMs: 0, + awaitRefresh: true, + }) + .pipe(Effect.provide(NodeServices.layer)); + assert.strictEqual(scans.length, 1); + assert.strictEqual(scans[0]?.dir, yield* Effect.promise(() => NodeFSP.realpath(opencode))); + assert.deepStrictEqual( + scans.flatMap( + (scan) => + scan.files?.flatMap((file) => file.records.map((record) => record.dedupeKey)) ?? [], + ), + ["opencode:msg-aliased"], + ); const service = yield* UsageService.make.pipe( Effect.provide( layerService({ prefix: "usage-service-aliased-roots-test", home, - settings, + settings: { + providerInstances: { + ...settings.providerInstances, + ...Object.fromEntries( + instances.map((instance) => [ + instance.instanceId, + { + driver: ProviderDriverKind.make("opencode"), + environment: Object.entries(instance.environment).map(([name, value]) => ({ + name, + value, + sensitive: false, + })), + }, + ]), + ), + }, + }, environment: { OPENCODE_DATA_DIR: `${opencode},${opencodeAlias}`, ANTIGRAVITY_DATA_DIR: `${antigravityA},${antigravityB}`, @@ -1051,6 +1115,10 @@ describe("UsageService", () => { summary.sources.filter((source) => source.fingerprint.provider === provider); assert.strictEqual(sourcesFor("opencode").length, 1); assert.strictEqual(sourcesFor("antigravity").length, 1); + assert.strictEqual( + summary.buckets.find((bucket) => bucket.provider === "opencode")?.totals.outputTokens, + 20, + ); assert.strictEqual( sourcesFor("opencode")[0]?.fingerprint.resolvedHomePath, yield* Effect.promise(() => NodeFSP.realpath(opencode)), diff --git a/packages/provider-opencode/src/server/usage.test.ts b/packages/provider-opencode/src/server/usage.test.ts index 697ac2854454..2c7ef13edb69 100644 --- a/packages/provider-opencode/src/server/usage.test.ts +++ b/packages/provider-opencode/src/server/usage.test.ts @@ -1,7 +1,3 @@ -// @effect-diagnostics nodeBuiltinImport:off - directory aliases use junctions on Windows, -// which Effect FileSystem.symlink does not expose. -import * as NodeFSP from "node:fs/promises"; - import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import { DEFAULT_SERVER_SETTINGS, ProviderInstanceId } from "@t3tools/contracts"; @@ -367,39 +363,6 @@ describe("openCodeUsageReader", () => { }).pipe(Effect.provide(NodeServices.layer)), ); - it.effect("scans canonical aliases shared by instances only once", () => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const temp = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-aliases-test-" }); - const home = yield* fileSystem.realPath(temp); - const root = path.join(home, "store"); - const alias = path.join(home, "alias"); - yield* seedHistory(root, "msg-shared"); - yield* Effect.tryPromise(() => NodeFSP.symlink(root, alias, "junction")); - const sources = yield* scanUsage([ - usageInstance("opencode-first", { OPENCODE_DATA_DIR: ` ${root}, ${alias} ` }), - usageInstance("opencode-second", { OPENCODE_DATA_DIR: alias }), - ]).pipe( - Effect.provideService(HostProcess.Environment, { - OPENCODE_DATA_DIR: path.join(home, "host"), - }), - ); - - assert.deepStrictEqual( - sources.map((source) => source.dir), - [root], - ); - assert.deepStrictEqual( - sources.flatMap( - (source) => - source.files?.flatMap((file) => file.records.map((record) => record.dedupeKey)) ?? [], - ), - ["opencode:msg-shared"], - ); - }).pipe(Effect.provide(NodeServices.layer)), - ); - it.effect.each([ { dataHome: "absolute", dataDirs: undefined }, { dataHome: "relative", dataDirs: " , " },