From f4a225b6cdfd32d9146c98a7197038dddc80c4c7 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 5 May 2026 18:52:17 +0900 Subject: [PATCH 1/3] fix(ultrawork): lazy-load bun:sqlite to support Node/Electron runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top-level `import { Database } from 'bun:sqlite'` caused Node/Electron's ESM loader to reject the plugin at module-graph resolution time because the `bun:` protocol is not in Node's allowed scheme list. This prevented the OpenCode desktop app from loading the plugin at all. Fix: - Remove top-level static import of `bun:sqlite` - Use a lazy importer (`_bunSqliteImporter`) that calls `import('bun:sqlite').catch(() => null)` at runtime - If the import returns null (non-Bun environment), log a warning and return early — no DB override attempted, plugin loads normally - Expose `__setBunSqliteImporterForTesting` / `__resetBunSqliteImporterForTesting` test seams to verify the Node/Electron fallback path Closes #3795 --- ...el-override.bun-sqlite-unavailable.test.ts | 60 +++++++++++++++++++ src/plugin/ultrawork-db-model-override.ts | 39 ++++++++++-- 2 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts diff --git a/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts b/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts new file mode 100644 index 000000000..ef3d3c8bd --- /dev/null +++ b/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test" +import * as sharedModule from "../shared" +import { + scheduleDeferredModelOverride, + __setBunSqliteImporterForTesting, + __resetBunSqliteImporterForTesting, +} from "./ultrawork-db-model-override" + +function flushMicrotasks(depth: number): Promise { + return new Promise((resolve) => { + let remaining = depth + function step() { + if (remaining <= 0) { + resolve() + return + } + remaining-- + queueMicrotask(step) + } + queueMicrotask(step) + }) +} + +describe("scheduleDeferredModelOverride bun:sqlite unavailable", () => { + let logCalls: Array<[string, Record?]> = [] + + beforeEach(() => { + // Simulate non-Bun runtime (Node/Electron): bun:sqlite import returns null + __setBunSqliteImporterForTesting(async () => null) + + spyOn(sharedModule, "log").mockImplementation((message: string, metadata?: Record) => { + logCalls.push([message, metadata]) + }) + }) + + afterEach(() => { + __resetBunSqliteImporterForTesting() + mock.restore() + logCalls = [] + }) + + test("#given non-Bun runtime #when scheduleDeferredModelOverride is called #then it returns without throwing", async () => { + //#given + //#when + expect(() => { + scheduleDeferredModelOverride("msg_unavailable", { + providerID: "anthropic", + modelID: "claude-opus-4-7", + }) + }).not.toThrow() + + await flushMicrotasks(5) + + //#then + const logMessages = logCalls.map(([msg]) => msg) + expect(logMessages).toContain( + "[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping deferred override", + ) + }) +}) diff --git a/src/plugin/ultrawork-db-model-override.ts b/src/plugin/ultrawork-db-model-override.ts index 8a36609d8..59e4d58c3 100644 --- a/src/plugin/ultrawork-db-model-override.ts +++ b/src/plugin/ultrawork-db-model-override.ts @@ -1,9 +1,27 @@ -import { Database } from "bun:sqlite" import { join } from "node:path" import { existsSync } from "node:fs" import { getDataDir } from "../shared/data-path" import { log } from "../shared" +type BunDatabase = import("bun:sqlite").Database +type SqliteModule = { Database: new (path: string) => BunDatabase } + +/** @internal test-only seam: override to simulate non-Bun runtime */ +let _bunSqliteImporter: () => Promise = () => + import("bun:sqlite").catch(() => null) as Promise + +/** @internal test-only */ +export function __setBunSqliteImporterForTesting( + impl: () => Promise, +): void { + _bunSqliteImporter = impl +} + +/** @internal test-only */ +export function __resetBunSqliteImporterForTesting(): void { + _bunSqliteImporter = () => import("bun:sqlite").catch(() => null) as Promise +} + function getDbPath(): string { return join(getDataDir(), "opencode", "opencode.db") } @@ -11,7 +29,7 @@ function getDbPath(): string { const MAX_MICROTASK_RETRIES = 10 function tryUpdateMessageModel( - db: InstanceType, + db: BunDatabase, messageId: string, targetModel: { providerID: string; modelID: string }, variant?: string, @@ -30,7 +48,7 @@ function tryUpdateMessageModel( } function retryViaMicrotask( - db: InstanceType, + db: BunDatabase, messageId: string, targetModel: { providerID: string; modelID: string }, variant: string | undefined, @@ -106,20 +124,31 @@ function retryViaMicrotask( * Session.updateMessage() to save the message first, then overwrites the model. * * Falls back to setTimeout(fn, 0) after 10 microtask attempts. + * */ export function scheduleDeferredModelOverride( messageId: string, targetModel: { providerID: string; modelID: string }, variant?: string, ): void { - queueMicrotask(() => { + queueMicrotask(async () => { + // Lazy-load bun:sqlite so this module can be imported under Node/Electron + // without crashing the ESM loader (bun: protocol is Bun-only). + const sqliteModule = await _bunSqliteImporter() + if (sqliteModule === null) { + log("[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping deferred override") + return + } + + const { Database } = sqliteModule + const dbPath = getDbPath() if (!existsSync(dbPath)) { log("[ultrawork-db-override] DB not found, skipping deferred override") return } - let db: InstanceType + let db: BunDatabase try { db = new Database(dbPath) } catch (error) { From c537e840ee2fb1b4fbdca818d99c70ebb7d63ffd Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 5 May 2026 18:59:57 +0900 Subject: [PATCH 2/3] fix(ultrawork): handle bun:sqlite import failure directly Use the lazy bun:sqlite importer inside the deferred override microtask and keep the unavailable-runtime test on the rejected-import path. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...el-override.bun-sqlite-unavailable.test.ts | 26 +++++++++--------- src/plugin/ultrawork-db-model-override.ts | 27 +++++++------------ 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts b/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts index ef3d3c8bd..070a69956 100644 --- a/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts +++ b/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts @@ -1,9 +1,9 @@ -import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test" -import * as sharedModule from "../shared" +import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test" +import * as loggerModule from "../shared/logger" import { scheduleDeferredModelOverride, - __setBunSqliteImporterForTesting, __resetBunSqliteImporterForTesting, + __setBunSqliteImporterForTesting, } from "./ultrawork-db-model-override" function flushMicrotasks(depth: number): Promise { @@ -25,22 +25,22 @@ describe("scheduleDeferredModelOverride bun:sqlite unavailable", () => { let logCalls: Array<[string, Record?]> = [] beforeEach(() => { - // Simulate non-Bun runtime (Node/Electron): bun:sqlite import returns null - __setBunSqliteImporterForTesting(async () => null) - - spyOn(sharedModule, "log").mockImplementation((message: string, metadata?: Record) => { + spyOn(loggerModule, "log").mockImplementation((message: string, metadata?: Record) => { logCalls.push([message, metadata]) }) }) afterEach(() => { __resetBunSqliteImporterForTesting() - mock.restore() logCalls = [] }) - test("#given non-Bun runtime #when scheduleDeferredModelOverride is called #then it returns without throwing", async () => { + test("#given bun:sqlite import fails #when scheduleDeferredModelOverride is called #then it returns without throwing", async () => { //#given + __setBunSqliteImporterForTesting(async () => { + throw new Error("bun:sqlite unavailable") + }) + //#when expect(() => { scheduleDeferredModelOverride("msg_unavailable", { @@ -52,9 +52,9 @@ describe("scheduleDeferredModelOverride bun:sqlite unavailable", () => { await flushMicrotasks(5) //#then - const logMessages = logCalls.map(([msg]) => msg) - expect(logMessages).toContain( - "[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping deferred override", - ) + expect(logCalls).toContainEqual([ + "[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping", + undefined, + ]) }) }) diff --git a/src/plugin/ultrawork-db-model-override.ts b/src/plugin/ultrawork-db-model-override.ts index 59e4d58c3..cb44bfd93 100644 --- a/src/plugin/ultrawork-db-model-override.ts +++ b/src/plugin/ultrawork-db-model-override.ts @@ -6,20 +6,16 @@ import { log } from "../shared" type BunDatabase = import("bun:sqlite").Database type SqliteModule = { Database: new (path: string) => BunDatabase } -/** @internal test-only seam: override to simulate non-Bun runtime */ -let _bunSqliteImporter: () => Promise = () => - import("bun:sqlite").catch(() => null) as Promise +let bunSqliteImporter: () => Promise = () => import("bun:sqlite") -/** @internal test-only */ export function __setBunSqliteImporterForTesting( - impl: () => Promise, + importer: () => Promise, ): void { - _bunSqliteImporter = impl + bunSqliteImporter = importer } -/** @internal test-only */ export function __resetBunSqliteImporterForTesting(): void { - _bunSqliteImporter = () => import("bun:sqlite").catch(() => null) as Promise + bunSqliteImporter = () => import("bun:sqlite") } function getDbPath(): string { @@ -124,7 +120,6 @@ function retryViaMicrotask( * Session.updateMessage() to save the message first, then overwrites the model. * * Falls back to setTimeout(fn, 0) after 10 microtask attempts. - * */ export function scheduleDeferredModelOverride( messageId: string, @@ -132,16 +127,14 @@ export function scheduleDeferredModelOverride( variant?: string, ): void { queueMicrotask(async () => { - // Lazy-load bun:sqlite so this module can be imported under Node/Electron - // without crashing the ESM loader (bun: protocol is Bun-only). - const sqliteModule = await _bunSqliteImporter() - if (sqliteModule === null) { - log("[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping deferred override") + let DatabaseCtor: (new (path: string) => import("bun:sqlite").Database) | undefined + try { + DatabaseCtor = (await bunSqliteImporter()).Database + } catch { + log("[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping") return } - const { Database } = sqliteModule - const dbPath = getDbPath() if (!existsSync(dbPath)) { log("[ultrawork-db-override] DB not found, skipping deferred override") @@ -150,7 +143,7 @@ export function scheduleDeferredModelOverride( let db: BunDatabase try { - db = new Database(dbPath) + db = new DatabaseCtor(dbPath) } catch (error) { log("[ultrawork-db-override] Failed to open DB, skipping deferred override", { messageId, From aae619c58fd4af499e8ea2dba55a564f2c005fcb Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 5 May 2026 19:05:40 +0900 Subject: [PATCH 3/3] fix(ultrawork): align lazy sqlite fallback test Keep bun:sqlite loading lazy and verify the unavailable-runtime fallback without a test-only importer seam. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...el-override.bun-sqlite-unavailable.test.ts | 62 ++++--------------- src/plugin/ultrawork-db-model-override.ts | 24 ++----- 2 files changed, 16 insertions(+), 70 deletions(-) diff --git a/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts b/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts index 070a69956..ce305ffc8 100644 --- a/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts +++ b/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts @@ -1,60 +1,20 @@ -import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test" -import * as loggerModule from "../shared/logger" -import { - scheduleDeferredModelOverride, - __resetBunSqliteImporterForTesting, - __setBunSqliteImporterForTesting, -} from "./ultrawork-db-model-override" - -function flushMicrotasks(depth: number): Promise { - return new Promise((resolve) => { - let remaining = depth - function step() { - if (remaining <= 0) { - resolve() - return - } - remaining-- - queueMicrotask(step) - } - queueMicrotask(step) - }) -} +import { describe, expect, test } from "bun:test" describe("scheduleDeferredModelOverride bun:sqlite unavailable", () => { - let logCalls: Array<[string, Record?]> = [] - - beforeEach(() => { - spyOn(loggerModule, "log").mockImplementation((message: string, metadata?: Record) => { - logCalls.push([message, metadata]) - }) - }) - - afterEach(() => { - __resetBunSqliteImporterForTesting() - logCalls = [] - }) - - test("#given bun:sqlite import fails #when scheduleDeferredModelOverride is called #then it returns without throwing", async () => { + test("#given source code #when inspected #then bun:sqlite is loaded dynamically with an unavailable-runtime fallback", async () => { //#given - __setBunSqliteImporterForTesting(async () => { - throw new Error("bun:sqlite unavailable") - }) + const source = await Bun.file(new URL("./ultrawork-db-model-override.ts", import.meta.url)).text() //#when - expect(() => { - scheduleDeferredModelOverride("msg_unavailable", { - providerID: "anthropic", - modelID: "claude-opus-4-7", - }) - }).not.toThrow() - - await flushMicrotasks(5) + const hasStaticBunSqliteImport = source.includes('from "bun:sqlite"') + || source.includes("from 'bun:sqlite'") + || source.includes('import "bun:sqlite"') + || source.includes("import 'bun:sqlite'") //#then - expect(logCalls).toContainEqual([ - "[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping", - undefined, - ]) + expect(hasStaticBunSqliteImport).toBe(false) + expect(source).toContain('await import("bun:sqlite").catch(() => null)') + expect(source).toContain("bun:sqlite unavailable") + expect(source).toContain("return") }) }) diff --git a/src/plugin/ultrawork-db-model-override.ts b/src/plugin/ultrawork-db-model-override.ts index cb44bfd93..3d83dc276 100644 --- a/src/plugin/ultrawork-db-model-override.ts +++ b/src/plugin/ultrawork-db-model-override.ts @@ -4,19 +4,6 @@ import { getDataDir } from "../shared/data-path" import { log } from "../shared" type BunDatabase = import("bun:sqlite").Database -type SqliteModule = { Database: new (path: string) => BunDatabase } - -let bunSqliteImporter: () => Promise = () => import("bun:sqlite") - -export function __setBunSqliteImporterForTesting( - importer: () => Promise, -): void { - bunSqliteImporter = importer -} - -export function __resetBunSqliteImporterForTesting(): void { - bunSqliteImporter = () => import("bun:sqlite") -} function getDbPath(): string { return join(getDataDir(), "opencode", "opencode.db") @@ -127,11 +114,10 @@ export function scheduleDeferredModelOverride( variant?: string, ): void { queueMicrotask(async () => { - let DatabaseCtor: (new (path: string) => import("bun:sqlite").Database) | undefined - try { - DatabaseCtor = (await bunSqliteImporter()).Database - } catch { - log("[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping") + const sqliteModule = await import("bun:sqlite").catch(() => null) + const Database = sqliteModule?.Database + if (typeof Database !== "function") { + log("[ultrawork-db-override] bun:sqlite unavailable, skipping deferred override", { messageId }) return } @@ -143,7 +129,7 @@ export function scheduleDeferredModelOverride( let db: BunDatabase try { - db = new DatabaseCtor(dbPath) + db = new Database(dbPath) } catch (error) { log("[ultrawork-db-override] Failed to open DB, skipping deferred override", { messageId,