anomalyco / anomalyco/opencode

plugins: transient first import failure permanently poisons plugin resolution until restart (Windows)

Open
#46,095 2 comments 0 reactions 1 assignee View on GitHub

@Hona is already working on this.

Since Aug 29, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

On Windows, if the first import of a newly created plugin file fails transiently (the config-watcher-triggered load races the file write), the failure is cached for the rest of the process lifetime: every later load of that plugin — including after the file is fixed, and regardless of the ?mtime= cache-buster query — fails with ResolveMessage: Cannot find module '...?mtime=...'. Only restarting the service clears it. A plugin that hits this once stays failed in opencode2 plugin list forever, with an error that makes it look like the file is missing even when it imports fine from a fresh process.

Environment

  • opencode version: 0.0.0-beta-18600
  • OS: Windows_NT 10.0.26200 (win32 x64)
  • Terminal: Unavailable: server-side issue
  • Shell: Unavailable: background service loads the plugin
  • Install/channel: beta (opencode2, shared background service)
  • Active plugins: global plugins dir ~/.config/opencode/plugins/ (auto-discovered); reproduced with a minimal file that has a valid V2 default-export object

Reproduction

  1. Run the opencode service, then create a new file ~/.config/opencode/plugins/probe.ts (valid V2 plugin: export default { id: "probe", async setup() {} }) via an editor or script that writes atomically (temp file + rename), which is what triggered it here.
  2. The watcher fires while the file is still settling; the server logs:
msg="loading plugin" id="...\plugins\probe.ts" entrypoint="C:/Users/.../plugins/probe.ts?mtime=1787978240100"
message="failed to load plugin" target="...\plugins\probe.ts" cause="Cause([Die(ResolveMessage: Cannot find module 'C:/Users/.../probe.ts?mtime=1787978240100' imported from B/~BUN/root/chunk-kwq4rck1.js)])"
  1. The file now exists and is valid. Touch it (new mtime), edit it, reload config — every attempt fails identically, including with a brand-new ?mtime= value.
  2. opencode2 plugin list shows the plugin as failed indefinitely.
  3. opencode2 service restart → the exact same file loads fine.

The underlying behavior reproduces in plain Bun 1.4.0 on Windows without opencode, which suggests the loader just inherits it:

const p = "C:/Users/me/AppData/Local/Temp/t3.ts"
try { await import(p + "?mtime=1") } catch {}   // fails: file does not exist yet
fs.writeFileSync(p, "export default 42")
await import(p + "?mtime=1")                    // STILL FAILS
await import(p + "?mtime=2")                    // STILL FAILS (new cache-buster)
await import(p)                                 // STILL FAILS (no query)
// only a fresh process imports it successfully

So Bun caches the failed resolution per file path for the process lifetime; the ?mtime= suffix busts the module/transpiler cache but not the cached resolution failure.

Expected Behavior

  • The first load of a newly created plugin file should not race the write that triggered the watcher (or should retry).
  • Once the file exists, subsequent loads (certainly after an mtime change) should resolve it.
  • A transient ENOENT-style failure should not permanently poison that plugin path for the process.

Actual Behavior

  • First import races file creation → transient ResolveMessage
  • Every subsequent import of that path fails instantly until the service restarts, regardless of ?mtime= value
  • Plugin status is stuck failed; the reported "Cannot find module" is misleading (the file exists and imports fine from a fresh process)

Additional Context

  • A second, smaller papercut from the same event: a plugin whose default export is a function (V1 shape) fails schema validation with SchemaError(Expected object at ["default"]), but if the resolution poisoning happens first, the ResolveMessage masks the shape error entirely — diagnosing which problem you have requires guessing.
  • Timing evidence from the incident: file created at 04:37:20.100, first import attempt 04:37:20.818 (0.7 s later), 8 concurrent activation attempts all failed; a fresh process imported the identical path?mtime specifier successfully.
  • Reproducible: the poisoning reproduced deterministically in isolated Bun; the opencode-level race is intermittent per file creation but was hit on the first new plugin file created today.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.