Skip to content

tui: Host.resolve treats Bun ResolveMessage as fatal for missing plugin subpaths #48122

Closed as not planned

Description

@DDwsgood

Description

packages/plugin/src/host.ts is intended to probe several conventional plugin entrypoints and continue when a subpath is missing. Under Bun 1.3.14, Bun.resolveSync throws a ResolveMessage for a missing module or package export. That object has a whitelisted code on its prototype, but it does not extend Error:

constructor: ResolveMessage
error instanceof Error: false
"code" in error: true
error.code: ERR_MODULE_NOT_FOUND
own properties: []

At source revision 9128e847bdbf9e5a58b7bdc33896e484e745aedd, packages/plugin/src/host.ts:25-38 first requires error instanceof Error. The first missing conventional subpath is therefore rethrown instead of being skipped.

Two real targets reproduce different points in the probe sequence:

local directory with index.ts but no server.ts:
Cannot find module '<plugin>/server'

[email protected], which exports ./server and ./tui but not ./rpc:
Cannot find module 'opencode-usage-stat/rpc'

The TUI plugin panel showed these targets as failed, while the server log shows both server entrypoints loading normally. Whether the packaged server's embedded Bun represents ResolveMessage in exactly the same way is uncertain; the deterministic reproduction and fix below use the source/dev TUI on system Bun 1.3.14, connected to the beta service.

A local fix replaced the instanceof Error gate with a string code check against the existing whitelist. After that change, both targets resolve as expected:

local: { server: "file://.../index.ts", tui: undefined, rpc: undefined }
usage-stat: { server: "file://.../dist/server.js", tui: "file://.../dist/tui.jsx", rpc: undefined }

Suggested fix in packages/plugin/src/host.ts:25-38: read (error as { code?: unknown }).code, require it to be a string, and apply the current missing-path whitelist without requiring error instanceof Error.

Plugins

  • Local opencode-enhanced-subagent directory (index.ts, no server.ts)
  • [email protected] (exports ., ./server, and ./tui, but no ./rpc)

OpenCode version

TUI source at 9128e847bdbf9e5a58b7bdc33896e484e745aedd, connected to opencode2 v0.0.0-beta-19271; Bun 1.3.14.

Steps to reproduce

  1. With Bun 1.3.14, call Bun.resolveSync("<local-plugin>/server", "<local-plugin>") for a plugin directory that has index.ts but no server.ts.
  2. Observe a ResolveMessage with code === "ERR_MODULE_NOT_FOUND" and instanceof Error === false.
  3. Call the current Host.resolve({ directory: "<local-plugin>" }) implementation. It throws on /server instead of falling back to /index.
  4. Alternatively, call Host.resolve for an installed package that exports server/TUI entrypoints but no ./rpc; it resolves the first two and then throws on the missing RPC export.
  5. Replace the instanceof Error gate with a typed string-code check and repeat. Both calls return complete Entrypoints objects without throwing.

Expected: missing optional/conventional subpaths whose error code is in the existing whitelist are skipped, regardless of whether Bun's thrown object extends Error.

Actual: every Bun ResolveMessage fails the instanceof Error check and becomes fatal.

Screenshot and/or share link

No screenshot. The exact probe output is included above.

Operating System

WSL2 Ubuntu, Linux 6.18.33.2-microsoft-standard-WSL2 x86_64.

Terminal

TERM=xterm-256color, shell /bin/bash.

Local verification notes

  • Reproduced both failing targets with the exact pre-fix predicate from packages/plugin/src/host.ts.
  • Applied and tested the narrow predicate change locally in commit 977afc6c74948533addda46f6653e9511146451f.
  • Re-ran both real-target probes successfully after the change.
  • Ran bun test test/host.test.ts from packages/plugin: 9 passed, 0 failed.
  • The claim that c72b535dee predates this path is not correct: that revision's packages/tui/src/plugin/context.tsx:607 already calls Host.resolve(target).tui. The guard itself was introduced by ed3259a9b7 before c72b535dee.

Activity

  1. DDwsgood commented on Sep 9, 2026

    @DDwsgood
    Author

    Related reports found while checking for duplicates (different mechanisms, listed for triage):

    Note: the instanceof Error gate analyzed here predates both of those reports (introduced by ed3259a9b7), so this is likely an independent cause.

  2. alvaropas17 commented on Sep 30, 2026

    @alvaropas17

    Same root cause, worse surface: a server-side plugin reload dies and registers zero agents

    I hit the exact Host.resolve gate described above, but on the server plugin-reload path instead of the TUI plugin panel. There the consequence is not "one target shown as failed" — the whole plugin generation dies, including builtins, so no agents are registered at all and the session fails with Agent not found <id>. Since it is the same predicate and the same suggested fix, I'm adding the evidence here rather than opening a duplicate.

    • Version/commit: 2.0.18, branch v2, HEAD 5bcf6b7 (built and run from source). Also reproduced on an installed binary dated 2026-09-27, and on a compiled binary.
    • Runtime: Bun, Windows.

    Summary

    Starting a new server from source with a config that includes any directory plugin lacking a server/index entrypoint ends with 0 agents and the agent call failing with Agent not found <id>. The server logs the fatal plugin-generation failure:

    level=ERROR message="failed to reload plugins"
      cause="Cause([Die(ResolveMessage: Cannot find module '<dir>\server' from '<dir>')])"
    

    Reproduced with <config>/plugins/quote-probe (contains only tui.ts) and with <dir>/opencode2-telegram (no server entrypoint).

    Impact

    The failure is total, not partial: because the rethrown ResolveMessage aborts the whole plugin generation, builtins are dropped too, so every agent disappears. The user-facing error (Agent not found <id>) gives no hint that a single optional subpath probe is the cause. This is the same symptom family reported in #48121 (Session.AgentNotFoundError after a failed reload), but here it is deterministic and has a concrete trigger.

    Steps to reproduce

    1. Configure a directory plugin that has no server/index entrypoint, e.g. a plugin dir containing only tui.ts:

      // <config>/opencode.json
      { "plugin": ["<config>/plugins/quote-probe"] }
    2. Start a fresh server from source (e.g. bun run … serve, private --standalone).

    3. Watch the server log: failed to reload plugins, cause="Cause([Die(ResolveMessage: Cannot find module '<dir>\server' from '<dir>')])".

    4. Run any agent: it fails with Agent not found <id>.

    Minimal, framework-free check of the underlying predicate (Bun):

    // Bun 1.x
    try {
      Bun.resolveSync("<plugin>/server", "<plugin>")
    } catch (e) {
      console.log(e.constructor.name, e instanceof Error, e.code)
    }
    // → ResolveMessage false ERR_MODULE_NOT_FOUND

    That is exactly the object the gate rejects.

    Root cause

    packages/plugin/src/host.ts → Host.resolve() tolerates a failed probe only when error instanceof Error and error.code is in { ENOENT, ENOTDIR, MODULE_NOT_FOUND, ERR_MODULE_NOT_FOUND, ERR_PACKAGE_PATH_NOT_EXPORTED, ERR_UNSUPPORTED_DIR_IMPORT }; anything else is rethrown.

    In the Bun runtime, resolveModule (packages/util/src/runtime/import.bun.ts) uses Bun.resolveSync, which throws a ResolveMessage with instanceof Error === false and code === "ERR_MODULE_NOT_FOUND". Because it does not pass the instanceof Error check, it is rethrown instead of skipped. Effect.sync then captures the throw as Die, killing the whole plugin generation (builtins included), which is why zero agents end up registered.

    Empirical verification of the mechanism (not inference): Bun.resolveSync("<dir>/server", "<dir>") on a missing path throws ResolveMessage with instanceof Error === false; wrapping the same failure in a real Error carrying the same code makes Host.resolve tolerate it and return the present entrypoints.

    Minimal fix

    Same as suggested in the issue above; either narrow change works:

    • (a) In Host.resolve, accept the missing-path condition based on a string error.code even when instanceof Error is false (read (error as { code?: unknown }).code, require it to be a string, apply the existing whitelist).
    • (b) In resolveModule (packages/util/src/runtime/import.bun.ts), rethrow a real Error preserving code.

    Note: a compiled binary hits the same path (it uses the same Bun condition / Bun.resolveSync), and the 2026-09-27 installed binary also returns 0 agents — so this is not source/dev-only.

    Workaround (temporary, not a fix)

    Until the predicate is fixed, avoid giving the probe a missing entrypoint: ensure each configured directory plugin has a server or index file (a stub server/index.ts is enough). Then Host.resolve never throws the fatal ResolveMessage.

    How this was surfaced (context)

    Found while validating an opt-in experiment, experimental.direct_exec — letting the shell tool accept executable + argv instead of a command string, so it can spawn a process directly (no PowerShell hop on Windows). A fresh server built with that experiment active had a directory plugin without a server entrypoint in config, which triggered the reload death above and masked the experiment for a while.

    Experiment files (all reverted on 2026-09-30, no PR): packages/schema/src/config/experimental.ts, packages/core/src/config/normalize.ts, packages/core/src/shell.ts, packages/core/src/shell/parse.ts, packages/core/src/tool/plugin/shell.ts, packages/plugin/src/effect/shell.ts, packages/plugin/src/promise/shell.ts; tests/bench created and removed: packages/core/test/direct-exec-parity.test.ts, packages/core/test/fixture/direct-exec.ts, packages/core/test/perf-direct-exec.harness.test.ts.

    A/B validation (private --standalone server from source, agent build, one run per path):

    Path Input Child of bun.exe Output Total Shell phase
    Normal (flag off) command="rg needle sample.txt ." bun.exe → powershell.exe → rg.exe needle here 8.961 ms 225 ms
    Direct (flag on) command=null, executable=<abs rg.exe>, argv=["needle","sample.txt"] rg.exe (direct, no PowerShell) needle here 9.326 ms 62 ms

    Result: PASS (exact input, no PowerShell in the tree, correct output). The saving is confined to the shell phase (225 → 62 ms, ~72 %); it does not show up in total turn time (n=1). Reported only as provenance for the plugin failure above — the direct_exec experiment itself is reverted and out of scope here.

  3. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and the probe output. I could only reproduce this on the Bun version you used, not on the one the repo targets:

    • On Bun 1.3.14, Bun.resolveSync throws a ResolveMessage that is not instanceof Error. Host.resolve on a local plugin with index.ts but no server.ts then throws Cannot find module '<dir>/server', exactly as you describe. This is still true on the latest dev (d1bd20fa).
    • On Bun 1.4.4, which the repo pins in package.json (packageManager: [email protected]), ResolveMessage is an Error. The same call returns { server: "file://.../index.ts", tui: undefined, rpc: undefined } without throwing.
    • The released CLI (@opencode/[email protected]) has Bun 1.4.4 built in, so it isn't affected either.

    So this only shows up when the source TUI runs on a Bun older than the pinned version. Upgrading Bun (bun upgrade) should fix it for your dev setup. If you still see plugins marked as failed with Bun 1.4.4, or with a released opencode binary, please reply with the version and the plugin setup. Your suggested check (any thrown value with a whitelisted string code) would still be a reasonable hardening for maintainers to consider.

    Related plugin-loading reports, which look like different causes: #58647, #49608, #50434, #46095.

  4. opencodejr commented on Oct 31, 2026

    @opencodejr
    Contributor

    Closing this as not planned since we haven't heard back. In our testing, Host.resolve only failed when the source TUI ran on Bun 1.3.14. That version throws a ResolveMessage that isn't an Error. On Bun 1.4.4, which the repo pins, and in the released @opencode/cli binary (it has Bun 1.4.4 built in), missing /server and /rpc subpaths are skipped as intended. Upgrading Bun should fix your dev setup.

    If plugins still show as failed with Bun 1.4.4 or a released opencode binary, please comment with the version and plugin setup and we can reopen this. Thanks again for the detailed report.

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