anomalyco / anomalyco/opencode

[Bug]: Plugin and provider loaders cache the same package under two keys and never refresh an existing install

Open
#48,514 3 comments 0 reactions 1 assignee View on GitHub

@rekram1-node is already working on this.

Since Sep 11, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
Avg merge
7h 2m
Merged PRs (30d)
384

Description

Summary

Opencode can keep loading an old version of a plugin/provider package even though npm reports the latest version as installed. Two things combine: the cache is keyed by the exact specifier string, and Npm.add returns an existing directory without checking whether it is still current.

Environment
  • OS: Ubuntu 24.04.4 LTS
  • opencode: 1.18.30
  • relevant package: opencode-cmd-provider (@latest dir at 1.7.3, bare dir at 1.6.2; npm reports 1.7.3 as latest)
Problem

1. Cache is keyed by the exact specifier string

Npm.add derives the cache directory from the raw package string:

// packages/core/src/npm.ts
const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg))

The two loaders pass different strings for the same package:

  • plugin loader appends @latest:

    // packages/opencode/src/plugin/shared.ts
    const pkg = hit?.name && hit.raw === hit.name ? `${hit.name}@latest` : spec
    
  • provider loader uses model.api.npm verbatim (whatever the plugin/provider declared):

    // packages/opencode/src/provider/provider.ts
    const item = await Npm.add(model.api.npm)
    

So a single plugin that registers its provider by bare name produces two directories:

~/.cache/opencode/packages/opencode-cmd-provider@latest   # plugin
~/.cache/opencode/packages/opencode-cmd-provider          # runtime provider

2. Existing installs are never refreshed

// packages/core/src/npm.ts
if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) {
  return resolveEntryPoint(name, path.join(dir, "node_modules", name))
}

Once the bare directory exists it is returned forever, at the version it was first installed. Updating the package moves only the @latest copy forward; the bare copy keeps the old version and is still what opencode loads at runtime.

Impact

The effective state is confusing and user-visible: npm reports the latest version as installed, but opencode runs the old one. Anything that depends on the two halves of one plugin matching — capability declarations vs. the code that enforces them, feature additions, bug fixes — can silently use stale behavior. Deleting the stale directory forces opencode to re-install the current version, which confirms the cause.

Why the plugin loader / provider loader should be reconciled

A plugin and the provider it registers are conceptually one dependency. Keying them separately and never reconciling versions makes divergence possible for any plugin that registers a provider by an unpinned (or differently formatted) specifier. A plugin author can work around this by pinning a version, but opencode should not depend on that discipline to keep a plugin internally consistent.

Possible directions
  • Key the cache by resolved package name (or name + resolved version), so foo and foo@latest resolve to the same installed artifact instead of two.
  • Have Npm.add verify the installed version against the requested spec and re-resolve when it no longer satisfies it, instead of short-circuiting on mere existence.
  • Ensure a plugin's runtime provider is loaded from the same resolved package the plugin came from.
Related

Plugin-side report (unpinned provider specifier makes this reachable): https://github.com/rashidrazak/opencode-cmd-provider/issues/149

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.