From 5f57aecdc4d9a9889a75068dd65e24af808adca5 Mon Sep 17 00:00:00 2001 From: Diane <35034946+dianedef@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:09:14 +0200 Subject: [PATCH] =?UTF-8?q?fix(opencode):=20don't=20export=20functions=20f?= =?UTF-8?q?rom=20the=20plugin=20=E2=80=94=20opencode=201.18.30=20loader=20?= =?UTF-8?q?crashes=20on=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit opencode's plugin loader treats any export whose value is a function as a plugin factory and crashes the server at startup ('plugin config hook failed (N.config)'). The plugin's named export runWithSpuriousRetry (added for the retry unit guard) therefore broke opencode launch entirely: 'Unexpected server error' before the first prompt. Expose it as a property on a const object instead ('retry.runWithSpuriousRetry') and point retry_test.ts at that. Reproduced standalone: opencode run fails to boot with the function-valued export present, boots cleanly (\pong\) without it; the retry guard still passes (ALL OK). --- backends/opencode/index.ts | 14 ++++++++++---- tests/backends/opencode/retry_test.ts | 4 +++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/backends/opencode/index.ts b/backends/opencode/index.ts index 184278c..717358c 100644 --- a/backends/opencode/index.ts +++ b/backends/opencode/index.ts @@ -79,10 +79,10 @@ const MAX_HOOK_ATTEMPTS = 3 // its cached time — a synchronous retry would re-read the same stale value and // fail again (which is exactly why the *next* hook in a burst always succeeds). // A genuine timeout takes ~HOOK_TIMEOUT_MS, far above SPURIOUS_TIMEOUT_MS, so it -// is never retried. `label` is used only for the diagnostic log. Exported so the -// retry behaviour can be exercised by the test harness (it's a Windows libuv -// quirk that can't otherwise be reproduced on CI). -export async function runWithSpuriousRetry( +// is never retried. `label` is used only for the diagnostic log. Exercised by +// retry_test.ts (it's a Windows libuv quirk that can't otherwise be reproduced +// on CI). +async function runWithSpuriousRetry( run: () => void, label = "hook-entry", ): Promise { @@ -116,6 +116,12 @@ export async function runWithSpuriousRetry( } } +// Exposed as a property on a plain object — NOT as a function-valued export — +// so retry_test.ts can still exercise it. opencode's plugin loader treats any +// export whose value is a function as a plugin factory and crashes the server +// at startup (opencode-ai 1.18.30, "plugin config hook failed (N.config)"). +export const retry = { runWithSpuriousRetry } + async function runHook(event: "pre" | "post", payload: object): Promise { const shim = resolveHookEntry() if (!shim) { diff --git a/tests/backends/opencode/retry_test.ts b/tests/backends/opencode/retry_test.ts index 7bbbf3f..7dac264 100644 --- a/tests/backends/opencode/retry_test.ts +++ b/tests/backends/opencode/retry_test.ts @@ -36,7 +36,9 @@ async function main(): Promise { // (the bare `D:\…` path is rejected by the ESM loader as an unsupported scheme). const indexPath = resolve(__dirname, "../../../backends/opencode/index.ts") const mod = await import(pathToFileURL(indexPath).href) - const runWithSpuriousRetry = mod.runWithSpuriousRetry as RunWithSpuriousRetry + // Exposed as `retry.runWithSpuriousRetry` (a property on a const object, not a + // bare function export — see backends/opencode/index.ts). + const runWithSpuriousRetry = mod.retry.runWithSpuriousRetry as RunWithSpuriousRetry // Success on the first attempt → run called exactly once. {