anomalyco / anomalyco/opencode
plugins: transient first import failure permanently poisons plugin resolution until restart (Windows)
@Hona is already working on this.
Since Aug 29, 2026.
- 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
- 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. - 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)])"
- 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. opencode2 plugin listshows the plugin asfailedindefinitely.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?mtimespecifier 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.