Skip to content

plugin: symlinked local plugin file never hot-reloads in a running server #51602

Closed as not planned

Description

@sblattj

Summary

A local plugin file that is a symlink never picks up edits while the server is running. The reload path correctly detects the change (the digest read follows the symlink) and re-imports the entrypoint, but the import returns the previously cached module — Bun caches it under the resolved realpath, while opencode invalidates its cache under the symlink path. The running server keeps the pre-edit module until the process restarts.

Environment

  • opencode version: 2.0.14
  • OS: macOS 15 (Darwin 25.5.0, darwin arm64)
  • Terminal: iTerm2 (TERM=xterm-256color)
  • Shell: zsh
  • Install/channel: latest (~/.opencode/bin/opencode)
  • Active plugins: two local file plugins auto-discovered from $XDG_CONFIG_HOME/opencode/plugins/ — one regular file (hot-reloads fine), one symlink (bug)

Reproduction

  1. In a scratch $XDG_CONFIG_HOME/opencode/plugins/ create two identical plugin files:
    • regular.ts — regular file: export default { id: "regular-probe", setup() {} }
    • linked.ts — a symlink to a file elsewhere (dotfiles checkout): same content, export default { id: "linked-probe", setup() {} }
  2. Start opencode serve, create a session, GET /api/plugin → both entries active.
  3. Edit both files (for the symlink, edit its target) to remove the default export:
    export const notDefault = 1
  4. GET /api/plugin again.

Expected Behavior

Both entries re-evaluate the new content and both flip to failed with Plugin must export a default definition with an id and an effect or setup function.

Actual Behavior

  • regular.ts: reloads — new content evaluated, state becomes failed (correct).
  • linked.ts: loading plugin is logged again (watcher + digest change both work — the digest is read through the symlink), but the import resolves to the cached pre-edit module; the entry stays active with the old id and old behavior. Only a server restart picks up the change.

Both files were edited in the same second, in the same server process:

02:17:24.249 msg="loading plugin" id=.../plugins/regular.ts
02:17:24.249 message="failed to load plugin" target=.../regular.ts   ← new content evaluated
02:17:24.558 msg="loading plugin" id=.../plugins/linked.ts
            (no failure, no change — import returned the stale module)

Root cause (traced in v2.0.14)

  • packages/plugin/src/source.bun.ts:12 — const root = fileURLToPath(entrypoint) is the symlink path.
  • packages/plugin/src/source.bun.ts:21 — delete require.cache[file] invalidates by that symlink path only.
  • The subsequent load (packages/plugin/src/source.ts:30-39 → packages/plugin/src/host.ts:46-48 → packages/util/src/runtime/import.bun.ts:3, a plain dynamic import()) resolves the module by its realpath, so the cache deletion misses and the stale module is returned.

Notably the node runtime path already solves this by appending a cache-busting __opencode_reload query param to every specifier (packages/plugin/src/source.node.ts:12-16); the bun path has no equivalent and relies solely on the require.cache deletion that misses symlinks.

Real-world impact: a plugin fixed from a v1-style shape to export default { id, setup } via a dotfiles-managed symlink kept the running service logging the schema failure on every /api/plugin poll for two days, while every fresh process loaded the fixed file fine — very confusing to debug since the on-disk file was already correct.

Workarounds

  • opencode service restart (or restart the serve process) after editing a symlinked plugin.
  • Keep regular files in plugins/ instead of symlinks.

Possible fix directions: resolve to realpath (fs.realpathSync) before the require.cache deletion for both root and tracked files, or add the bun equivalent of the __opencode_reload cache-busting suffix used on the node path.

Activity

  1. sblattj commented on Sep 27, 2026

    @sblattj
    Author

    Possibly the same underlying pattern (symlink path taken literally instead of resolving to realpath) in a sibling component: #39738 (symlinked global opencode.json silently ignored). Cross-linking in case a shared canonicalization fix is preferable to per-component patches.

  2. github-actions commented on Sep 27, 2026

    @github-actions
    Contributor

    This issue does not fully meet our contributing guidelines.

    What needs to be fixed:

    • Please use the Bug Report issue template and include its required Description field. The issue currently uses a Summary heading instead.

    Please edit this issue to address the above within 2 hours, or it will be automatically closed.


    This issue might be a duplicate of existing issues. Please check:

    • #34492: proposes unified file watching and hot reload, including plugin files.
    • #44389: proposes explicit plugin reload and discusses cache-busting Bun imports.
    • #39987: requests applying plugin and config changes without restarting.
    • #39738: reports a symlinked global config being ignored due to symlink-path handling.

    If you believe this was flagged incorrectly, please let a maintainer know.

  3. github-actions commented on Sep 27, 2026

    @github-actions
    Contributor

    This issue has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

    Feel free to open a new issue that follows our issue templates.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions