Skip to content

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

Open

Description

@BlacKSnowDot0

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.

Activity

  1. github-actions commented on Aug 29, 2026

    @github-actions
    Contributor

    This issue might be related to an existing issue. Please check:

    • [BUG] 「V2 (next-17444)」local .ts plugins fail to load on Windows #42763: Same ResolveMessage: Cannot find module '...?mtime=...' error on Windows with local .ts plugins. That issue covers the compiled binary failing to resolve ?mtime= specifiers at all; your report is distinct (the resolution failure is cached after a transient race), but they share the same error surface and Windows-only scope — worth cross-referencing to avoid duplicate fix efforts.
  2. MilkyWay008 commented on Aug 29, 2026

    @MilkyWay008

    Classic Bun negative-cache, it memoizes the failed module lookup per path so your ?mtime= buster does nothing, the ENOENT is what got cached not the file. The minimal Bun 1.4.0 repro you built is the gold here, that's a genuine upstream bug worth filing, restart of the service is just the only clean eviction in the meantime. Oh also, btw, I built a portable Hermes OTG and I run it on USB as my rescue agent; super handy doing quick diagnose & fix, check it, imo it'll help you. https://github.com/MilkyWay008/Hermes-OTG

  3. MonNa-free commented on Sep 29, 2026

    @MonNa-free

    Another instance of the same "failure cached for process lifetime" behavior, but at a different stage — so it likely won't be fixed by the Bun import-cache fix alone:

    • Environment: Linux (Ubuntu 22.04), opencode v2.0.19 (latest channel), serve --service background process
    • Failure stage: plugin loads and imports fine, but fails later in skill.transform with SchemaError: Missing key at ["path"] → disabled plugin after transform failure
    • Same poisoning symptom: after fixing the plugin file on disk, serve never re-attempted the load. GET /api/plugin kept returning the pre-fix failure (status: failed with the stale ref from before the fix). Closing/reopening the TUI had no effect since the CLI just reconnects to the same serve process.
    • API probes (all no-ops for this state): POST /api/plugin/check returns the list unchanged; POST /api/plugin/update with valid targets returns empty, state unchanged; /api/plugin/reload, /api/plugin/refresh, /api/plugin/enable → 404
    • Recovery: only killing the opencode serve --service process and restarting clears it

    So the process-lifetime caching applies to post-import validation failures too, not just Bun's negative module cache. An explicit reload/retry route (as requested in #44389 → #39987) would cover both cases.

    (Filed this separately as #52061 before finding this thread — closing it as a duplicate and keeping the details here.)

  4. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the very thorough report and the minimal Bun snippet.

    I tried it in a Linux sandbox with Bun 1.4.2, the version current opencode embeds. The first import of the missing file failed with the same ResolveMessage: Cannot find module '...?mtime=1'. Once the file existed, all three later imports (?mtime=1, ?mtime=2 and no query) loaded it fine. So on Linux, Bun doesn't keep the failed lookup, and the plugin loader recovers.

    That points to Windows-specific Bun behaviour, and I can't test Windows here, so this needs a maintainer with a Windows machine to confirm. One thing that would help: does your plain-Bun snippet still fail with a recent Bun (1.4.2 or newer)? Until this is looked at, opencode2 service restart is the workaround.

    Related: #57658 (a Windows plugin kept failing with ENOENT on an existing file until a service restart) was closed as a duplicate of this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions