Skip to content

fix(server): ensure Plugin service materialises at serve startup - #41728

Closed
herjarsa wants to merge 3 commits into
anomalyco:devfrom
herjarsa:fix/plugin-serve-init-clean
Closed

herjarsa wants to merge 3 commits into
anomalyco:devfrom
herjarsa:fix/plugin-serve-init-clean

Conversation

@herjarsa

@herjarsa herjarsa commented Aug 11, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #38470

Type of change

  • Bug fix

What does this PR do?

HTTP serve mode (\opencode serve, used by OpenChamber, desktop and web clients) never materialises the Plugin service. The serve command declared \instance: false\ ("no need for an ambient project InstanceContext at startup"), so the lazy \InstanceState\ used by \plugin/index.ts\ is only built when \Plugin.init/trigger/list\ is called. In HTTP mode that never happens for the conversation pipeline, so \plugin.server(input)\ is never invoked. Result: plugins listed in \plugins:\ (e.g. omo-meta-governor) silently do nothing under serve — no hooks, no custom tools, no system/messages transforms — while the same plugins work fine in the TUI and \opencode run.

Fix: switch the serve command to \instance: true\ with \directory: () => process.cwd(). The \effectCmd\ wrapper (\src/cli/effect-cmd.ts) already loads the InstanceStore and runs InstanceBootstrap (which calls \Plugin.init()) at startup when \instance\ is \ rue. Per-request \x-opencode-directory\ handlers still load their own InstanceStore entries (different ScopedCache key), so the ambient cwd is only a warm-up, not a replacement of per-request semantics. The handler is \Effect.never, so the wrapper's \ inally { store.dispose(ctx) }\ never runs while the serve is alive and the loaded plugin state persists for the process lifetime.

How did you verify your code works?

  • \�un run --cwd packages/opencode typecheck\ passes (\ sgo --noEmit\ exit 0).
  • Empirically repro'd the bug on 1.18.15/1.18.16 before the change: \POST /session\ + \POST /session/:id/message\ against a fixture project whose plugin logs \MetaGovernor plugin loaded\ (module-scope import) but never \ actory_invoked\ / \config_loaded\ (the plugin's \server(input)\ is never called). TUI and \opencode run\ with the same bundle do log \ actory_invoked\ — confirming this is a serve-mode init bug, not a plugin bug.
  • Detailed repro + source evidence in PROPOSAL-server-plugin-init.md (included in this PR).

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. needs:issue labels Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

…upstream anomalyco#38470

Empirical repro: opencode serve (1.18.15/1.18.16) imports the plugin bundle (module scope 'plugin loaded') but never materializes the lazy InstanceState, so plugin.server(input) is never invoked. Identical bundle works fine in TUI / opencode run.

Proposal document at PROPOSAL-server-plugin-init.md lays out the reproducer, source evidence, the fix sketch (warm InstanceStore for process.cwd() at serve startup), tests to add, and reviewer notes tied to upstream issue anomalyco#38470. No source code is changed in this commit — the fix is intentionally kept as a proposal so the smallest possible diff can be reviewed against the v2 Effect refactor before merging.
@herjarsa
herjarsa force-pushed the fix/plugin-serve-init-clean branch from d4f0a16 to 48dc42d Compare August 11, 2026 11:37
@github-actions github-actions Bot removed needs:issue needs:compliance This means the issue will auto-close after 2 hours. labels Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@brad07

brad07 commented Aug 18, 2026

Copy link
Copy Markdown

Thanks for picking this up — and to be clear up front, I'm not disputing the plugin fix. The instance: false → instance: true change looks right for the problem you reproduced, and the plugin-side evidence (factory_invoked / config_loaded missing under serve) is convincing.

But I think Closes #38470 is the wrong link, and I'd rather flag that before it merges than after.

#38470 is about MCP server tools, not plugins. I filed it, and the symptom was that a configured stdio MCP server shows connected on GET /mcp but its tools never reach a session created through the HTTP API. That's a different subsystem from Plugin.Service, and the PR's verification doesn't exercise it.

I built the reproducer against your branch to check whether the fix happens to cover it anyway. It doesn't. Method: a fixture project with a one-tool stdio MCP server, and a local Anthropic-compatible endpoint standing in for the model so I could capture the exact tools array opencode sends — no model spend, and it removes any ambiguity about "did the model just decline to call it".

Tree / version Request pingtest_ping_test offered?
1.18.3 POST /session/:id/message ✅ yes
1.18.3 POST /api/session/:id/prompt ❌ no
1.18.18 POST /api/session/:id/prompt ❌ no
this PR's base (instance: false) POST /api/session/:id/prompt ❌ no
this PR (instance: true) POST /api/session/:id/prompt ❌ no

Row 1 is the control: same fixture, same server, tools present — so the MCP config and the harness are working, and the misses below it are real.

The distinguishing factor is the /api surface, not the ambient instance. /api/ paths go through workspace routing (middleware/workspace-routing.ts:167 → selectedV2WorkspaceID) and resolve a per-request directory; /session doesn't. That matches what I saw in the original DEBUG logs — a per-request creating instance for the project directory — and it's why an ambient warm-up for process.cwd() doesn't reach it.

Worth noting this also means #38470 is unfixed on latest (1.18.18), not just on 1.18.3.

Ask: could you change Closes #38470 to a plain reference (or point it at a plugin-specific issue)? Then this can merge on its own merits without silently closing an unrelated, still-live bug. Happy to share the full reproducer — it's small, and it discriminates cleanly.

@JanUlrichR

JanUlrichR commented Aug 20, 2026 •

Copy link
Copy Markdown

Hey, can I somehow help to get this fix merged? This issue kind of blocks our use-case right now.

@herjarsa

Copy link
Copy Markdown
Author

Confirming the plugin-side symptom from @herjarsa/[email protected] (V2 plugin registered in \opencode.json's \plugin\ array).

Reproduction:

  • CLI (\opencode\ TUI or \opencode run): all \omo_*\ tools register, all hooks fire. Verified locally on Windows 11 + bun 1.3.14.
  • OpenCode Desktop (spawns \opencode serve\ in HTTP/sidecar mode): zero \omo_*\ tools in the session, hooks never fire. Plugin module-loads (we see [meta-governor] v0.38.2 MetaGovernor plugin loaded\ in \meta-governor.log\ from the module-scope \logToFile\ call) but \createMetaGovernorPlugin(...).server(input)\ is never invoked.

This is independent of \omo-meta-governor\ itself — we shipped 4 v0.38.x releases assuming the plugin was loading in Desktop and only learned otherwise when users reported missing tools. So we have a concrete user-visible regression class that maps cleanly to \Plugin.service\ never being materialised in serve mode.

@brad07 is right that \Closes #38470\ looks wrong (the original bug is MCP-server-tools-in-via-API, different subsystem). The plugin fix stands on its own — just needs to be unlinked from #38470 so it can merge and we can declare \peerDependencies\ floor on OpenCode Desktop with confidence.

Happy to ship a regression test in \omo-meta-governor\ that exercises the serve-mode init sequence against a mocked \Plugin.service\ and fails when \plugin.server(input)\ is not called — that would document the contract from downstream's perspective. Filed #42280 as the canonical tracking issue if it's helpful for cross-reference.

herjarsa added a commit to herjarsa/omo-meta-governor that referenced this pull request Aug 29, 2026
OpenCode Desktop spawns 'opencode serve' in HTTP/sidecar mode. In some
versions, the plugin service is never materialised during serve startup,
so plugin.server(input) is never called and the plugin's omo_* tools
never appear in the session — even though the module loads (you can see
'MetaGovernor plugin loaded' in meta-governor.log).

Two changes:

1. New regression test in src/plugin.test.ts: 'serve-mode plugin init
   contract (v0.38.5)'. Documents the @opencode-ai/plugin contract from
   the plugin author's POV:
   - createMetaGovernorPlugin() must return a callable Plugin function
   - plugin(input, options) must return a Promise<Hooks> with the 4 standard
     hook keys (tool.execute.before, tool.execute.after,
     experimental.chat.system.transform, experimental.chat.messages.transform)
   - The bundled entry (dist/index.js) must export a callable default

   These tests pass today because we exercise the factory directly.
   When upstream PR #41728 lands, the test should be extended to cover
   the new 'opencode serve' integration test shipped with that PR.

2. README.md 'Known limitation: OpenCode Desktop plugin init (v0.38.5)'
   section. Links to the upstream tracking:
   - anomalyco/opencode#42280 (V2 plugin init race)
   - anomalyco/opencode#41728 (PR with the fix, currently OPEN)
   - anomalyco/opencode#44367 (Desktop-specific npm plugin issue)

   Documents the workaround: use opencode CLI (plugin loads normally) or
   enable the MCP server mode (omo_* tools via sidecar process).

Also posted a comment on PR #41728 (comment ID 5461960479) confirming our
case from the plugin author's side and requesting the author separate
'Closes #38470' (which is wrong — it's an MCP-tools issue, not a plugin
issue) so the PR can merge.

Test count: 1094 pass / 0 fail / 0 error (1102 total, 92 files).
No production code changed.
@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP tools missing from sessions created via POST /api/session on opencode serve (present in the TUI)

3 participants