Skip to content

fix(opencode): support the OpenCode 2 plugin loader and event stream (#20407) - #20410

Closed
liobr wants to merge 4 commits into
stablyai:mainfrom
liobr:fix/opencode-v2-plugin-loader
Closed

liobr wants to merge 4 commits into
stablyai:mainfrom
liobr:fix/opencode-v2-plugin-loader

Conversation

@liobr

@liobr liobr commented Sep 12, 2026

Copy link
Copy Markdown

Summary

Fixes #20407.

OpenCode 2's external plugin loader decodes the module default export against { id, setup } (or { id, effect }) and ignores server(), so the generated orca-opencode-status.js failed to load with:

PluginModule.LoadError: Plugin must export a default definition with an id and an effect or setup function.
(cause: SchemaError(Missing key at ["default"]["effect"] Missing key at ["default"]["setup"]))

Satisfying the loader alone is not enough: OpenCode 2 emits session.* / form.* events with a data envelope, while the engine consumes the V1 events (message.part.updated, session.status, permission.asked, question.asked, …) with a properties envelope. So this change also maps the V2 stream onto the existing engine.

What the change does:

  • Keeps the named factory export and the default server() for OpenCode 1.x.
  • Adds setup(ctx) to the default export for OpenCode 2.x.
  • Adds status-plugin-v2-compat-source.ts, which maps the V2 stream onto V1 events:
    • session.execution.started → busy; .succeeded / .interrupted / .failed → idle (live V2 runs do not emit session.status for ordinary turn boundaries; both forms are still passed through when present)
    • session.status / session.idle passthrough
    • session.text.started / .delta / .ended → role cache, busy recovery, and assistant previews
    • session.inbox.enqueued → user prompt preview
    • permission.asked → PermissionRequest with the V1 field names the listener reads (permission, patterns, metadata, always, tool)
    • permission.replied / form.created / form.replied / form.cancelled → attention resolution and AskUserQuestion
  • Presents the V2 context through a small client facade shaped like the SDK generation the engine's lineage walk expects, so child sessions still roll up to the root pane.

Live evidence (OpenCode 2.0.1, Orca hooks enabled)

Full successful turn captured at the hook endpoint:

SessionStart -> MessagePart:user -> SessionBusy -> MessagePart:assistant -> SessionIdle

An auto-rejected tool ask produced one PermissionRequest with permission=shell, patterns=["echo hello-from-probe"], and the mapped metadata/tool fields. The plugin loads with no failed to load plugin entry.

Testing

  • pnpm exec vitest run --config config/vitest.config.ts src/main/opencode/ — 9 files, 123 tests pass (8 new V2 behavior tests)
  • pnpm run typecheck:node
  • pnpm exec oxlint <changed files> — 0 warnings/errors
  • pnpm build — not run (heavy desktop build)

Notes

OpenCode 2 decodes the module default export against `{ id, setup }` (or
`{ id, effect }`) and ignores `server()`, so the generated status plugin
never loaded and OpenCode pane status stayed dark. V2 also emits
`session.*` and `form.*` events with a `data` envelope instead of the V1
events the engine consumes.

- Default export now carries both `server` (1.x) and `setup` (2.x).
- New V2 compat source maps the V2 stream onto the V1 events: execution
  boundaries to busy/idle, text events to previews, inbox enqueue to the
  user prompt, permission/form events to attention, plus status/idle
  passthroughs.
- The V2 context is exposed as the SDK client shape the lineage walk
  expects so child sessions still roll up to the root pane.

Verified live against OpenCode 2.0.1: a full turn posts SessionStart,
MessagePart(user), SessionBusy, MessagePart(assistant), SessionIdle, and
an auto-rejected permission ask posts PermissionRequest.

Fixes stablyai#20407
@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: a412211d-ca10-4d49-898a-579ec270bb1a

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa0405 and a68feaf.

📒 Files selected for processing (1)
  • src/main/opencode/hook-plugin-opencode-v2.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/opencode/hook-plugin-opencode-v2.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The generated OpenCode plugin now exports both server() for OpenCode 1.x and setup() for OpenCode 2.x. New generated adapters translate V2 session, message, permission, and form events into the existing V1 hook events. The setup path normalizes session access, subscribes to V2 events, handles cleanup, and logs errors. Contract, integration, mapping, lineage, and form-resolution tests were added or updated.

Priority: ⬆️ High

Severity of issue fixed: High

Merge Risk: ⚪ Minimal · up to a68fe

The compatibility change has no identified current-head merge-blocking risk.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: OpenCode 2 plugin loader and event-stream support.
Description check ✅ Passed The description clearly explains the problem, implementation, linked issue, compatibility behavior, live evidence, and test results. It does not use all template headings, and it omits the explicit Vi…
Linked Issues check ✅ Passed The changes satisfy the coding requirements in issue #20407. status-plugin-factory-source.ts generates a default export with id, legacy server(), and OpenCode 2 setup(). `status-plugin-v2-even…
Out of Scope Changes check ✅ Passed The changes remain within issue #20407. The compatibility source, generated plugin updates, contract tests, integration tests, and source digest updates directly support dual loader compatibility and …
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 8 files.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The V2 adapter is written against a plugin/event surface that does not match the published @opencode-ai/plugin V2 API, and its form.* resolution can leave a question stuck in waiting. Please confirm the first against the exact OpenCode 2.0.1 surface you tested before merging — details inline.

Reviewed changes

  • Adds setup() to the plugin default export (status-plugin-factory-source.ts) so OpenCode 2.x's { id, setup } / { id, effect } loader accepts the module while keeping server() for 1.x.
  • New V2→V1 event adapter (status-plugin-v2-compat-source.ts): maps session.status / session.idle / session.execution.* / session.text.* / session.inbox.enqueued / permission.* / form.* onto the V1 properties shapes the engine consumes, and exposes a session.get / session.list facade for the lineage walk.
  • Wires the adapter into the generated plugin (hook-service.ts) and bumps the source hash pin in hook-service.test.ts.
  • New tests (hook-plugin-opencode-v2.test.ts, 9 cases) plus a setup() assertion in hook-plugin-module-contract.test.ts.

ℹ️ Nitpicks

  • The form→question mapper emits Orca's output in OpenCode's own Question shape (multiple/custom), but Orca's parseQuestionsShape reads multiSelect (src/shared/native-chat-ask.ts:42), so a V2 multi-select form still renders and validates as single-select. If card parity is intended, map to multiSelect (and drop custom, which nothing reads).
  • session.created is reconstructed from a flat data.parentID, while the engine keys child detection on info.parentID; only a root session.created is covered. A child event that nests or omits the parent would post a spurious SessionStart and poison rootSessionById (status-plugin-session-lineage-source.ts:10), defeating child suppression. A child-session test would pin this.
  • The two new tests disagree on setup's return type (Promise<unknown> in the module-contract test vs a disposer here); if the loader ignores the return, the returned disposer is dead code and the subscription is never aborted.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using openrouter/deepseek/deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

- Map multiselect form fields to `multiSelect`, the key Orca's question
  parser reads, instead of the SDK's `multiple`.
- Mirror the permission requestID hedge on form.replied/form.cancelled so a
  blocker still clears when the id arrives as requestID.
- Warn once instead of silently no-oping when the V2 event API is absent,
  and guard the session facade when the domain is unavailable.
- Split the compat source into event-mapping and setup fragments to stay
  under the 300-line max; generated bytes are unchanged.
- Add child session.created, form.replied and form.cancelled tests.
@liobr

liobr commented Sep 13, 2026

Copy link
Copy Markdown
Author

Pushed baffb0ef to address the rest of the review:

  • multiSelect: the form mapper now emits multiSelect: true for multiselect fields (the key parseQuestionsShape reads) and drops the unread custom; the form test asserts it.
  • Child session.created: added a test that a child event posts nothing. The mapper builds info.parentID from the flat V2 data.parentID, and the test pins that root detection still works.
  • setup return type: aligned the module-contract test type with the published Cleanup contract, matching the V2 test.
  • File size: the compat source is split into status-plugin-v2-event-source.ts and status-plugin-v2-setup-source.ts to stay under the 300-line max. Generated bytes are unchanged, so the hash pins still pass.

Final state: 126 tests pass across src/main/opencode, typecheck:node clean, oxlint clean on the changed files.

@liobr

liobr commented Sep 13, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Mapped V2 form questions to multiSelect in status-plugin-v2-event-source.ts, dropping the unread multiple/custom keys so multi-select forms render and validate correctly (parseQuestionsShape reads multiSelect).
  • Hedged form resolution IDs — form.replied / form.cancelled now use data.requestID || data.id, mirroring permission.replied, so a blocker whose id arrives as requestID still clears.
  • Split the V2 compat source into status-plugin-v2-event-source.ts (V2→V1 event mapping) and status-plugin-v2-setup-source.ts (client facade + subscription pump), wiring both into getOpenCodeFamilyPluginSource.
  • Hardened setup(ctx) with a console.warn when ctx.event is absent and a null-safe createOrcaV2Client session facade, so a shape mismatch is visible rather than a silent no-op.
  • Added tests for child session.created suppression, form.replied, and the requestID resolution hedge; the V2 suite now has 12 cases.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@nwparker

Copy link
Copy Markdown
Contributor

Thanks for the PR! OpenCode 2 support is on main through #21418 (ee354a3) and its follow-ups. The generated plugin's default export carries setup (setupOpenCode2Status) next to server(), and the factory source normalizes the V2 session.next.* / permission.v2 / question.v2 events. #20407 is closed. Closing — please reopen if there's a remaining case main doesn't cover.

@nwparker nwparker closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: orca-opencode-status.js fails to load on OpenCode 2.x — V2 loader requires setup()/effect(), and V2 events differ from the V1 hooks

2 participants