From c867e66ab5d3bb70a9b32fd8f5bf250310bc86b3 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 10 Jul 2026 11:21:43 -0400 Subject: [PATCH] feat(plugin): add promise plugin cleanup --- packages/core/src/plugin/promise.ts | 4 +++- packages/core/src/plugin/skill/opencode.md | 22 +++++++++--------- packages/core/test/plugin/promise.test.ts | 27 ++++++++++++++++++++++ packages/docs/build/plugins.mdx | 23 ++++++++++++++++-- packages/plugin/src/v2/promise/README.md | 9 ++++++++ packages/plugin/src/v2/promise/plugin.ts | 4 +++- 6 files changed, 74 insertions(+), 15 deletions(-) diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index ec379e2ce60..716d69c10d5 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -130,7 +130,9 @@ export function fromPromise(plugin: PromisePlugin) { }, } - yield* Effect.promise(() => Promise.resolve(plugin.setup(context2))) + const cleanup = yield* Effect.promise(() => Promise.resolve(plugin.setup(context2))) + if (!cleanup) return + yield* Effect.addFinalizer(() => Effect.promise(() => Promise.resolve(cleanup()))) }), }) } diff --git a/packages/core/src/plugin/skill/opencode.md b/packages/core/src/plugin/skill/opencode.md index e6d89a0c3e4..6cc6dc50551 100644 --- a/packages/core/src/plugin/skill/opencode.md +++ b/packages/core/src/plugin/skill/opencode.md @@ -4,10 +4,10 @@ Use this guide as the starting point for work involving OpenCode itself. It covers the core concepts needed to configure and customize OpenCode, extend it with plugins, and build integrations with the OpenCode SDK, clients, and API. -Full documentation is available at . Consult +Full documentation is available at . Consult it when this overview does not contain enough detail for the task. -## Configuration +## [Configuration](https://v2.opencode.ai/config) OpenCode configuration uses JSON or JSONC. Include the published schema so the user's editor can validate fields and provide autocomplete: @@ -34,14 +34,14 @@ Do not guess field names or shapes. Use as the source of truth and preserve unrelated settings when editing an existing file. -See the [full configuration guide](https://opencode.mintlify.site/config) for +See the [full configuration guide](https://v2.opencode.ai/config) for every field, examples, config locations, and links to dedicated feature guides. -## V1 to V2 migration +## [V1 to V2 migration](https://v2.opencode.ai/migrate-v1) For any request to migrate OpenCode configuration, agents, commands, skills, plugins, integrations, or other behavior from V1 to V2, read the full -[migration guide](https://opencode.mintlify.site/migrate-v1) before acting. In +[migration guide](https://v2.opencode.ai/migrate-v1) before acting. In the repository, its source is `packages/docs/migrate-v1.mdx`. V1 config files and `.opencode/` definitions are intended to remain compatible. @@ -54,7 +54,7 @@ not attempt migration yet. Once the V2 plugin API is finalized, OpenCode should be able to migrate most plugins. If non-API V1 functionality fails in V2, use the `report` skill to file it as a compatibility bug. -## Service +## [Service](https://v2.opencode.ai/troubleshooting#check-the-background-service) OpenCode uses a client-server architecture. Interfaces such as the TUI connect to a background OpenCode service, which owns sessions, configuration, plugins, @@ -73,7 +73,7 @@ Check its status after restarting: opencode2 service status ``` -## API +## [API](https://v2.opencode.ai/api) OpenCode exposes an HTTP API from its server. The API is described by an OpenAPI document available from the running server at `/openapi.json`. @@ -101,12 +101,12 @@ connected to an explicit server instead of its managed background service, use the same configured server and authentication context rather than constructing an unauthenticated request separately. -See the [full API reference](https://opencode.mintlify.site/api) for available +See the [full API reference](https://v2.opencode.ai/api) for available endpoints, parameters, request bodies, and response schemas. The -raw [OpenAPI specification](https://opencode.mintlify.site/openapi.json) is also +raw [OpenAPI specification](https://v2.opencode.ai/openapi.json) is also available for code generation and other tooling. -## Troubleshooting +## [Troubleshooting](https://v2.opencode.ai/troubleshooting) OpenCode runs a client and a background server. Start by determining whether a problem belongs to the client, the shared server, or one project. @@ -124,6 +124,6 @@ problem belongs to the client, the shared server, or one project. - Redact API keys, authorization headers, prompts, file contents, and other sensitive data before sharing diagnostics. -See the [full troubleshooting guide](https://opencode.mintlify.site/troubleshooting) +See the [full troubleshooting guide](https://v2.opencode.ai/troubleshooting) for service lifecycle commands, API inspection, log locations, explicit server connections, issue-reporting details, and local development paths. diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index e521545db62..433341d3989 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -97,6 +97,33 @@ describe("fromPromise", () => { }), ) + it.effect("runs the setup cleanup when the plugin scope closes", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + const host = yield* PluginHost.make(plugin) + const events: string[] = [] + const promisePlugin = Plugin.define({ + id: "promise-cleanup", + setup: async () => { + events.push("setup") + return async () => { + await Promise.resolve() + events.push("cleanup") + } + }, + }) + + yield* Effect.scoped( + Effect.gen(function* () { + yield* PluginPromise.fromPromise(promisePlugin).effect(host) + expect(events).toEqual(["setup"]) + }), + ) + + expect(events).toEqual(["setup", "cleanup"]) + }), + ) + it.effect("constructs plain Promise tool declarations in the host", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service diff --git a/packages/docs/build/plugins.mdx b/packages/docs/build/plugins.mdx index c697f7a122c..0b609db5d20 100644 --- a/packages/docs/build/plugins.mdx +++ b/packages/docs/build/plugins.mdx @@ -148,7 +148,25 @@ export default Plugin.define({ ``` `setup` runs each time the plugin is activated. Register long-lived behavior -during setup; do not wait there on an infinite event stream. +during setup; do not wait there on an infinite event stream. It may return a +synchronous or asynchronous cleanup function. OpenCode awaits that cleanup +when the plugin is disabled, reloaded, or shut down: + +```ts +setup: async (ctx) => { + const controller = new AbortController() + const task = synchronize(ctx, controller.signal) + + return async () => { + controller.abort() + await task + } +} +``` + +Hook registrations are released automatically with the same plugin scope. Use +the returned cleanup for resources the plugin owns, such as timers, watchers, +connections, and background tasks. ### Context @@ -216,7 +234,8 @@ export default Plugin.define({ } await refresh() - setInterval(() => void refresh().catch(console.error), 60_000) + const timer = setInterval(() => void refresh().catch(console.error), 60_000) + return () => clearInterval(timer) }, }) ``` diff --git a/packages/plugin/src/v2/promise/README.md b/packages/plugin/src/v2/promise/README.md index 8d2984bf929..f94f18d060b 100644 --- a/packages/plugin/src/v2/promise/README.md +++ b/packages/plugin/src/v2/promise/README.md @@ -25,6 +25,15 @@ export default Plugin.define({ ``` Plugin setup registers hooks imperatively through each domain's `hook` method. +It may return a synchronous or asynchronous cleanup function. OpenCode awaits +the cleanup when the plugin is unloaded or replaced: + +```ts +setup: async (ctx) => { + const timer = setInterval(refresh, 60_000) + return () => clearInterval(timer) +} +``` Configuration supplied for the plugin is available as `ctx.options`. diff --git a/packages/plugin/src/v2/promise/plugin.ts b/packages/plugin/src/v2/promise/plugin.ts index 37bce3cc031..2e05b1fded0 100644 --- a/packages/plugin/src/v2/promise/plugin.ts +++ b/packages/plugin/src/v2/promise/plugin.ts @@ -26,9 +26,11 @@ export interface Context { readonly tool: ToolDomain } +export type Cleanup = () => Promise | void + export interface Plugin { readonly id: string - readonly setup: (context: Context) => Promise | void + readonly setup: (context: Context) => Promise | Cleanup | void } export function define(plugin: Plugin) {