Skip to content

Plugin reload during a turn fails the step retry and marks every skill as removed #51128

Closed

Description

@kernel-oops

Description

If a plugin reloads while a turn is running (e.g. a local plugin file is edited), a step retry that lands during the reload reads a half-rebuilt catalog. The turn ends as failed without calling the provider again (we have also seen Model unavailable: <provider>/<model> for a model defined in config), and the session gets a permanent system instruction saying every skill "is no longer available and must not be used".

The runner waits for plugins.awaitActivation once per drain, but each step and retry goes back through prepareContext → context.select / resolveModel without waiting again. Possibly the same root cause as #47114, which also happened just after a plugin reload.

Plugins

One local plugin (plugins: [{ "package": "./plugins/probe" }]) whose setup awaits 3 seconds; the delay only widens the window.

OpenCode version

v2.0.16 (also the current v2 branch)

Steps to reproduce

  1. Config with a provider/model defined in opencode.json and the local plugin above.
  2. Point the provider at a local stub that answers the first request with HTTP 500 and later ones with 400.
  3. Send a prompt. As soon as the stub sees the first request, append a comment to the plugin file.
  4. The retry runs during the reload: the stub never receives a second request, the turn ends failed, and the session now contains "The following skill IDs are no longer available and must not be used: …".

Waiting for plugin activation in prepareContext (and in manual compaction's resolveContext) fixes it: the retry waits for the reload, then reaches the provider normally.

Screenshot and/or share link

n/a

Operating System

Debian 13 (Linux 6.12)

Terminal

n/a (reproduced over the HTTP API with opencode serve)

Activity

  1. Just-Silver commented on Sep 25, 2026

    @Just-Silver

    Confirming on v2.0.15 (Windows): hot-reloading a local plugin during a turn reproduces both symptoms

    Repro: a local plugin configured via plugins (a directory). Edit any file under its src/** while a session turn is running (this triggers the plugin hot-reload). Observed:

    1. The turn dies and never reaches the provider again

    level=ERROR message="Failed to drain Session"
    cause="SessionRunnerModel.ModelUnavailableError: Model unavailable: r4-coder/deepseek-v4.1-flash
        at SessionRunnerModel.resolve
        at SessionContext.load
        at SessionRunner.advanceToStep
        at SessionRunner.drain"
    

    The model is defined in opencode.json (a custom provider), i.e. a config-defined model — same shape as #47114.

    2. The session then carries the "all skills removed" instruction

    The following skill IDs are no longer available and must not be used:
    api-and-interface-design, bootstrapblazor, brainstorming, code-simplification, ...
    

    followed later by a system update re-adding them.

    This is consistent with the description: a step/retry re-enters prepareContext → context.select / resolveModel while the catalog is half-rebuilt, after the runner already waited for activation once per drain.

    Notes

    • It is intermittent and usually self-heals on the next location boot, but while it lasts the turn ends failed and any auto-continuation loop (e.g. a goal plugin driving multi-turn work) stops until the model resolves again.
    • Reproduced over the HTTP API with opencode serve; no provider stub needed — the failure happens before any provider request.

    +1 for #51133 (await plugin activation in prepareContext, and in manual compaction's resolveContext).

  2. JKratto commented on Oct 10, 2026

    @JKratto

    Confirming on v2.0.26 (macOS, managed background service). Hit it 4 times today, in two
    different sessions — both were editing files under a watched plugin directory
    (~/.config/opencode/vendor/...) during their own turn when it happened.

    Every occurrence has the exact reported shape:

    • turn dies at SessionRunner.advanceToStep → SessionRunnerModel.resolve with
      SessionRunnerModel.ModelUnavailableError for a config-declared provider that was working
      fine seconds before and after;
    • in both sessions the failure lands in the same second as the "following skill IDs are no
      longer available" system instruction — matches the half-rebuilt-catalog theory;
    • the session parks idle mid-work with a completed tool result and no client-visible error;
      a manual "Continue." prompt re-resolves the model and resumes normally.

    Log sample:

    level=ERROR message="Failed to drain Session"
    cause="SessionRunnerModel.ModelUnavailableError: Model unavailable: <provider>/<model>
        at SessionRunnerModel.resolve ... at SessionRunner.advanceToStep ... at SessionRunner.drain"
    

    So: reproducible trigger = plugin/vendor file write during a running turn. Fix PR #54012
    describes exactly this ("prevent transient plugin registry teardown from stopping an
    already-running session") — was it closed for a reason, or does it still need a reviewer?

  3. mradwankhalil commented on Oct 11, 2026

    @mradwankhalil
    Contributor

    Confirming on v2.0.25 (Windows 11, OpenChamber-managed opencode serve), with one addition: the provider that vanished was not edited at all. Editing an earlier-ordered plugin is enough, because of the prefix rule in Plugin.activate.

    What happened (2026-10-11, two occurrences, both the exact stack from this issue):

    18:46:54.49Z  <config>/plugins/squeez.js rewritten on disk (auto-discovered plugin, ordered before the configured ones)
    18:46:55.9Z   "loading plugin" for every plugin, in ~10 live locations
    18:46:56–57Z  model.updated in every location (12 in that minute; baseline is 1–5/min)
    18:46:59.02Z  ERROR "Failed to drain Session"
                  SessionRunnerModel.ModelUnavailableError: Model unavailable: claude-code/claude-opus-5-5
                    at SessionRunnerModel.resolve
                    at SessionContext.load
                    at SessionRunner.advanceToStep
                    at SessionRunner.drain
    
    18:50:53–59Z  another local plugin's dist/ rebuilt (also ordered before the provider plugin)
    18:51:09.93Z  same error, same stack, a different session
    

    claude-code is a provider registered by a third plugin (ctx.provider.transform, activation: "enabled"), whose files did not change. It still disappeared, because activate keeps only the unchanged prefix alive ("Registrations are ordered by setup, so only the unchanged prefix can stay alive"): every plugin after the changed one has its scope closed, then each is set up again in order. State.batch only coalesces notifications ("Reads inside stay fresh"), so until the provider plugin's own setup reaches provider.transform, any reader sees a catalog without it. That gap is the sum of the setup time of every plugin ordered before it, plus whatever its own setup awaits first.

    The runner side is as described above: plugins.awaitActivation runs once in drain (session/runner/llm.ts), while every later step goes advanceToStep → prepareContext → context.load → SessionRunnerModel.resolve without waiting again. A session in a multi-step tool loop only has to cross a step boundary inside the gap. The failure is terminal for the turn, so in the UI it reads as "the provider disconnected".

    Two things make this much easier to hit than "someone edited a plugin during a turn" suggests:

    +1 for awaiting activation in prepareContext and in the manual-compaction context.select / resolveModel path. Retrying ModelUnavailableError once after awaitActivation would cover any remaining reader as well.

    Plugin-side mitigation for anyone hitting this meanwhile: call ctx.provider.transform first in setup, before any await on I/O (ours awaited a port bind and a CLI probe of up to 3 s before registering). It narrows the gap but cannot close it, since earlier plugins still set up first.

  4. robertn702 commented on Oct 14, 2026

    @robertn702

    Still reproduces on 2.0.28 and 2.0.29, and #54012 was closed without merging, so there's no fix in flight that I can see.

    Two things I haven't seen in this thread yet.

    The reload doesn't need anyone to edit a plugin. Any write to a config directory's plugin/ or plugins/ folder triggers it. In my case Orca rewrote its config overlay during an orca worktree create that the session itself ran, and the next step failed.

    The failure has a second shape. If the reloaded plugin is ordered after mine, my plugin stays registered but the post plugins (opencode.config.provider and the rest) get disposed. The provider my plugin registers still resolves, but the config models it wraps are gone, so it failed with source model ... not found for all five of them.

    I reproduced both shapes against a real opencode binary in an isolated HOME with a loopback fake upstream. With a 3s reload window each fails 3/3 on 2.0.28 and 2.0.29, and with no delay both complete 3/3. The script needs my plugin built, but the harness is generic. Script is repro-issue-76.mjs. Notes are in issue-76.md.

  5. rebelliard commented on Oct 16, 2026

    @rebelliard

    Confirming on v2.0.25 (macOS, OpenChamber-managed opencode serve). Three hits in one server run today, same stack every time (SessionRunner.drain → advanceToStep → SessionContext.load → SessionRunnerModel.resolve → ModelUnavailableError), each for a different config-declared model under the same custom provider (vercel/vmc/daily-economy, daily-balance, daily-intelligence).

    One more trigger for the list: a git operation run by the session's own tool call. No plugin file was edited. The branch switch rewrote .opencode/opencode.json (one line, a different pin for the same plugin), AGENTS.md, and two symlinks under .agents/skills/. That alone reloads every plugin and rebuilds the catalog mid-turn.

    Timeline of the clearest one (all in the same session):

    01:56:35.089  shell tool: git stash push -u … && git switch main
    01:56:35.976  watcher stopped  .agents/skills            (reload #1 starts)
    01:56:36.217  loading plugin   ×7 (local + 2 github: plugins)
    01:56:36.319  event config.updated
    01:56:36.497  ERROR Failed to drain Session
                  SessionRunnerModel.ModelUnavailableError: Model unavailable: vercel/vmc/daily-economy
    01:56:36.562  event provider.updated                     (65 ms after the failure)
    01:56:36.570  event model.updated
    01:56:36.693  loading plugin   ×7 again                  (reload #2: skills dir and config file change separately)
    01:56:51      user typed "retry" → model resolves, turn continues
    

    The other two had the same shape: git reset --hard … && git rebase origin/main from the session's tool call 1.3 s before the error (different worktree), and one reload in a sibling worktree whose trigger I did not pin down. In all three, provider.updated landed 50–300 ms after the ERROR line.

    What the user sees: the tool result completes, the session goes idle with no visible error, plus the "The following skill IDs are no longer available and must not be used" system update followed by a second update re-adding them. "retry" resumes normally.

    Two notes for anyone reproducing:

    • opencode --version on PATH said 2.0.29 while the long-running server was still 2.0.25, so check the server's version, not the CLI's.
    • Any repo that commits .opencode/opencode.json and switches between branches with different plugin pins will hit this on every checkout, so this is not limited to people editing plugins.

    +1 for awaiting activation in prepareContext (or retrying ModelUnavailableError once after awaitActivation).

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

Metadata

Metadata

Assignees

No one assigned

    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