Skip to content

server: failed plugin reload on config change silently drops custom agents and commands until restart #49982

Closed

Description

@Nuokfish

Summary

On v2.0.9 running as the background service (opencode serve --service, desktop app), config edits repeatedly fail with ERROR failed to reload plugins — TypeError: pe is not a function. After these failed reloads, the live service silently loses all file-discovered custom agents and commands: /api/agent and /api/command (and the TUI pickers) degrade to builtins only until the service is restarted. A freshly started server on the identical config loads everything, so the definitions and config are valid.

Environment

  • opencode version: 2.0.9
  • OS: Darwin 27.0.0 (darwin arm64), macOS
  • Terminal: OpenCode desktop app (session host); TERM_PROGRAM=Apple_Terminal, TERM=xterm-256color
  • Shell: /bin/zsh
  • Install/channel: latest; binary at ~/.opencode/bin/opencode, background service mode
  • Active plugins: none in final state (transient local plugins were registered and removed during the session that triggered the bug; config has no plugin key afterwards)

Reproduction

  1. Set up custom agents (e.g. ~/.config/opencode/agent/review.md) and commands (project .opencode/commands/*.md ×23, global ~/.config/opencode/commands/*.md ×1). Start the background service and confirm via /api/agent / /api/command that they are loaded.
  2. With the service running (active sessions, MCP servers connected, two locations booted: home + project), edit ~/.config/opencode/opencode.json repeatedly — in my case registering then removing local plugin entries, including a moment where a registered plugin's files were deleted before its config key was removed.
  3. Each reload logs: ERROR failed to reload plugins cause="Cause([Die(TypeError: pe is not a function. (In 'pe()', 'pe' is an instance of Object))])"
  4. Afterwards /api/agent returns only the 7 builtins (custom review agent gone) and /api/command returns only the 2 builtins (all 24 custom commands gone). No error is logged at the moment the catalogs degrade — the loss is silent.
  5. opencode service restart → everything comes back.

Honest trigger caveat: on an isolated opencode serve (no --service, no connected clients) I could not trigger the crash with an mtime touch, a byte-identical rewrite, or adding a valid no-op plugin entry. It reproduced only on the shared --service instance with active sessions and multiple booted locations, which points at the concurrent/multi-location reload path also seen in #48121.

Expected Behavior

A failed plugin reload should not take down the agent/command catalogs, and file-discovered agents/commands should remain registered. Note the agent/command directories are not subscribed by any watcher (only skills and config files are), so once the catalogs degrade there is no self-healing path short of a restart.

Actual Behavior

Log excerpts (paths trimmed; timestamps UTC):

13:07:26 ERROR failed to reload plugins cause="Cause([Die(TypeError: pe is not a function. (In 'pe()', 'pe' is an instance of Object))])"
13:25:02 ERROR failed to reload plugins cause=… (same defect, recurs ≥10× over ~30 min)
13:35:19 WARN  failed to load plugin target="<project>/.opencode/plugin/aios-canary" cause="ENOENT …"   (plugin files removed before config key)
13:35:19 ERROR failed to reload plugins cause=… (same TypeError, ×3 in <200 ms)

After this window, on the still-running service:

/api/agent    → build, compaction, explore, general, plan, summary, title   (custom "review" agent gone)
/api/command  → init, review                                                 (23 project + 1 global commands gone)

Same binary + same config on a fresh opencode serve: 8 agents (incl. review) and 26 commands load correctly — verified twice on isolated instances.

Additional Context

Activity

  1. Nuokfish commented on Sep 19, 2026

    @Nuokfish
    Author

    Workaround applied on the reporting machine: opencode service restart at 2026-09-19 21:55:57 CST; will attach post-restart catalog verification shortly.

  2. Nuokfish commented on Sep 19, 2026

    @Nuokfish
    Author

    Post-restart verification on the reporting machine: agents = build, compaction, explore, general, plan, review, summary, title; commands = 26 commands: archive-project, check-work, deliver-closeout, force, generate-prd, generate-report, generate-spc, grill-me, init, log-time, log-work, migrate-log, next-step, quick-log, refresh-daily-insights, refresh-dashboard-insights, review, review-all, review-artifact, review-evolution, review-project, start-project, startup, suspend-project, switch-project, sync-company-dictionary; verdict = PASS (review agent restored, project commands restored).

  3. holny commented on Sep 20, 2026

    @holny
    Contributor

    Dug into the plugin supervisor after seeing this. The TypeError: pe is not a function side looks like the legacy-export problem already tracked in #50021, but the durable defect is that a failed activation takes down the whole generation - file-discovered agents and commands included - and supervisor.ts:229 only logs the die, so with nothing watching the agent/command dirs there's no way back until restart. Happy to take this one. Plan: keep the last-good generation on a failed activation (treat the bad entry as a per-plugin failure like the duplicate-ID path already does) and add a regression test for the reload-failure path. Should have a PR in a day or two.

  4. holny commented on Sep 21, 2026

    @holny
    Contributor

    Quick update before opening the PR: keeping the last-good generation on a failed activation turns out to collide with semantics the registry tests pin down on purpose. disables plugins after replay failures discovered during setup without restoring the old generation (packages/core/test/plugin.test.ts:349) asserts that a failing revision tears the previous activation down, and the lifecycle tests require stops to fire before starts, which a keep-previous fallback breaks. So a naive fallback contradicts existing tested behavior rather than filling a gap. Would you rather see failed reloads retain the previous generation (revisiting those tests), or should the fix stay narrower — supervisor retrying from the last good packages while the registry semantics stay as-is?

  5. holny commented on Sep 24, 2026

    @holny
    Contributor

    Following up on my question above — either answer works for me: keep-last-good if that's the intended semantics (I'll revisit the registry test that pins removal), or I'll go with the narrow fix (keep 61dce72334's applySafe + a regression test that the catalog survives a failed reload). Just don't want to guess wrong on intent before opening the PR.

  6. holny commented on Sep 26, 2026

    @holny
    Contributor

    Still happy to take this in whichever direction you prefer — the narrow fix (applySafe + catalog-survival regression test, commit already on my branch) is a day of latency away from a PR, and the keep-last-good route is fine too if that's the intended semantics. Otherwise I'll go with the narrow fix when I next pick this up.

  7. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I could reproduce this on 2.0.9 with a plain opencode serve; --service, the desktop app and active sessions aren't needed. It still happens on the latest dev (62ae125).

    What triggers it here: a local plugin whose setup returns an object instead of a cleanup function, e.g. setup: async () => ({}).

    1. Add the plugin to opencode.json. Custom agents and commands are still listed.
    2. Remove it from the config. Unloading it tries to call that object as the cleanup. The log shows failed to reload plugins … TypeError: … is not a function. (In '…()', '…' is an instance of Object), which matches your error. On dev the unminified name is cleanup.
    3. Afterwards, /api/agent no longer has the custom review agent and /api/command returns only init and review, in every booted location. Nothing else is logged.

    The script below reproduces it. One difference from what you saw: here, a later unrelated config edit restored the agents and commands without a restart.

    The transient plugins you registered may have returned an object from setup, for example a hooks object. If so, that would explain the error.

    Closely related: #48121, where a different reload error also leaves agents unavailable until restart.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #49982: failed plugin reloads under a live server, then check agent/command catalogs.
    # Usage: OC="opencode" bash repro.sh   (or OC="bun ... packages/cli/src/index.ts")
    set -u
    OC=${OC:-opencode}
    R=${R:-/tmp/opencode/r49982}; rm -rf $R; mkdir -p $R/h/.config/opencode/{agent,commands} $R/proj/.opencode/commands $R/plug
    export HOME=$R/h XDG_CONFIG_HOME=$R/h/.config XDG_DATA_HOME=$R/h/.local/share XDG_STATE_HOME=$R/h/.local/state XDG_CACHE_HOME=$R/h/.cache
    printf -- '---\ndescription: review agent\nmode: subagent\n---\nReview code.\n' > $R/h/.config/opencode/agent/review.md
    printf -- '---\ndescription: g\n---\nglobal cmd\n' > $R/h/.config/opencode/commands/gcmd.md
    for i in 1 2 3; do printf -- "---\ndescription: p$i\n---\nproj $i\n" > $R/proj/.opencode/commands/p$i.md; done
    echo '{}' > $R/h/.config/opencode/opencode.json
    (cd $R/proj && git init -q)
    mkplug(){ mkdir -p $R/plug/$1; cat > $R/plug/$1/index.ts <<P
    // v1-style habit: setup returns an object (hooks) instead of a cleanup function
    export default { id: "$1", setup: async () => ({}) }
    P
    }
    PORT=${PORT:-4211}
    q(){ curl -s -u opencode:pw -H "x-opencode-directory: $2" localhost:$PORT/api/$1 | jq -c '[.data[].name]'; }
    (cd $R/proj && OPENCODE_SERVER_PASSWORD=pw $OC serve --port $PORT --print-logs --log-level info > $R/serve.log 2>&1) &
    SRV=$!; for i in $(seq 40); do sleep 1; [ "$(q agent $R/proj 2>/dev/null)" != "[]" ] && [ -n "$(q agent $R/proj 2>/dev/null)" ] && break; done
    check(){ echo "[$1] proj agents=$(q agent $R/proj) cmds=$(q command $R/proj)"; echo "[$1] home agents=$(q agent $R/h) cmds=$(q command $R/h)"; }
    check before
    CFG=$R/h/.config/opencode/opencode.json
    mkplug canary
    echo "{\"plugin\":[\"$R/plug/canary\"]}" > $CFG; sleep 3
    check "plugin added"
    echo '{}' > $CFG   # remove plugin: unloading it calls the returned object as a cleanup
    sleep 4
    check after
    echo '{"username":"x"}' > $CFG; sleep 4   # another, unrelated config edit
    check "after another edit"
    grep -E 'ERROR|failed to (re)?load' $R/serve.log | cut -c1-300 | sort | uniq -c | head -20
    kill $SRV

    Run it: bash repro.sh

  8. holny commented on Nov 10, 2026

    @holny
    Contributor

    PR #57775 fixes this trigger: the plugin teardown was calling whatever setup() returned as a cleanup function, and the repro's setup: async () => ({}) plugin is exactly the case it guards — the value is now ignored unless it's an actual function, so the reload no longer aborts and the agent/command catalogs stay intact. Once it merges this repro should pass without the restart.

  9. gandb commented on Nov 12, 2026

    @gandb

    Confirming this on 2.2.7, Linux, plain opencode in MATE Terminal (VTE 0.76.0, TERM_PROGRAM unset) — no Orca, no desktop app. Same signature, and one thing broader than the file-discovered case described above.

    The agents are defined inline, in the agents block of ~/.config/opencode/opencode.json — there is no ~/.config/opencode/agent/ORACLE.md on this machine. So the loss is not limited to file-discovered agents: agents that live in the config file itself are dropped by the failed reload as well, and nothing re-reads them afterwards.

    Nine occurrences today (2026-11-12), each within seconds of a failed reload:

    ERROR message="failed to reload plugins" cause="Cause([Die(TypeError: o is not a function. (In 'o()', 'o' is an instance of Object))])"
    ERROR message="Failed to drain Session"
      cause="Session.AgentNotFoundError: Agent not found: \"ORACLE\""
        at SessionContext.select
        at SessionRunner.prepareContext
        at SessionRunner.advanceToStep
        at SessionRunner.drain
        at ServerProcess.start
    
    reload event (local time) failure
    10:21:43 failed to reload plugins 10:21:43 · 10:23:57
    10:59:34 failed to reload plugins 10:59:36 · 10:59:59 · 11:00:35
    11:04:21 failed to reload plugins 11:04:22
    11:40:27 plugin reconciliation 11:40:27 · 11:41:17
    11:47:57 failed to reload plugins 11:48:16

    A second inline agent (ORCHESTRATOR) fails identically, so it is not one agent name.

    The part that matters most, and matches the expected behaviour here. The trigger was momentary: the config file was briefly bad (a backup restored over ~/.config/opencode/ replaces opencode.json as a whole), the reload failed, and the open sessions lost their agent. The file then came back to a perfectly valid copy — and nothing recovered: the sessions stayed without their agent, and the TUI kept showing ORACLE · <model> in the status bar while the server answered Agent not found. Only a restart brought it back.

    Expected, as the reporter put it:

    • a config file that is momentarily unreadable or invalid must not change the agent a running session uses;
    • once the file is valid again, the session must recover the configured default_agent by itself, with no restart;
    • restoring a backup over the config folder must not silently wipe the configuration of the windows that are already open.

    Previously reported as #62002 and closed as not planned with an Orca-only explanation; this is the same defect, reproduced without Orca.

    Close-up from the MATE Terminal session — Error: Agent not found: "ORACLE" (marked 1) with the status bar still reading ORACLE · DeepSeek V4.1 Flash 1017:

    MATE Terminal showing Agent not found: ORACLE
  10. gandb commented on Nov 13, 2026

    @gandb

    Tested on Linux (x64) with v2.3.0 (tag a6b15d33), which includes ec263d62b6 (#63548). Confirmed by ancestry: the fix commit is an ancestor of the release tag (58 commits ahead, 0 behind).

    Before — a failed plugin reload (TypeError: o is not a function) left the running service without its custom agents. Every following turn failed with Session.InternalError: Session.AgentNotFoundError: Agent not found: "ORACLE" until a full restart. Reproduced two ways:

    • inside an Orca terminal (Orca runs opencode serve --service and the TUI attaches);
    • in a plain terminal with no Orca at all, with the agents defined inline in opencode.json.

    After upgrading to v2.3.0 — the agents survive the reload:

    • no restart needed anymore, neither inside Orca nor in the plain TUI;
    • no AgentNotFoundError in the log;
    • the session keeps working normally after an invalid config file is written and then restored.

    So the failing-finalizer path is gone for me. Thanks for the fix.

    For the record: #62002 was closed by triage as not_planned because the earlier repro depended on reload timing, but the non-recovering case was this same failing-finalizer bug — the "restore agents/ORACLE.md" workaround from that thread did not apply to agents defined inline in opencode.json.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions