1
0
Эх сурвалжийг харах

fix(core): reload local plugins on file changes

Dax Raad 1 сар өмнө
parent
commit
6ffecf9345

+ 20 - 16
packages/core/src/plugin.ts

@@ -1,8 +1,9 @@
 export * as PluginV2 from "./plugin"
 
+import type { Plugin } from "@opencode-ai/plugin/v2/effect"
+import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
 import { makeLocationNode } from "./effect/app-node"
 import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
-import { Plugin } from "@opencode-ai/schema/plugin"
 import { AgentV2 } from "./agent"
 import { AISDK } from "./aisdk"
 import { Catalog } from "./catalog"
@@ -18,14 +19,8 @@ import { State } from "./state"
 import { ToolRegistry } from "./tool/registry"
 import { ToolHooks } from "./tool/hooks"
 
-export const ID = Plugin.ID
-export type ID = typeof ID.Type
-export const Info = Plugin.Info
-export type Info = Plugin.Info
-export const Event = Plugin.Event
-
 export interface Interface {
-  readonly activate: (plugins: readonly import("@opencode-ai/plugin/v2/effect").Plugin[]) => Effect.Effect<void>
+  readonly activate: (plugins: readonly { readonly plugin: Plugin; readonly version?: string }[]) => Effect.Effect<void>
   readonly list: () => Effect.Effect<Info[]>
 }
 
@@ -36,16 +31,20 @@ const layer = Layer.effect(
   Effect.gen(function* () {
     const events = yield* EventV2.Service
     const scope = yield* Scope.make()
-    const active = new Map<ID, Scope.Closeable>()
+    const active = new Map<typeof ID.Type, Scope.Closeable>()
     const lock = Semaphore.makeUnsafe(1)
-    let generation: readonly ID[] | undefined = []
-    let host: Parameters<import("@opencode-ai/plugin/v2/effect").Plugin["effect"]>[0]
+    let generation: readonly { readonly id: typeof ID.Type; readonly version?: string }[] | undefined = []
+    let host: Parameters<Plugin["effect"]>[0]
 
     const activate = Effect.fn("Plugin.activate")(function* (
-      plugins: readonly import("@opencode-ai/plugin/v2/effect").Plugin[],
+      plugins: readonly { readonly plugin: Plugin; readonly version?: string }[],
     ) {
-      const definitions = plugins.map((plugin) => ({ ...plugin, id: ID.make(plugin.id) }))
-      const ids = new Set<ID>()
+      const definitions = plugins.map((entry) => ({
+        ...entry.plugin,
+        id: ID.make(entry.plugin.id),
+        ...(entry.version === undefined ? {} : { version: entry.version }),
+      }))
+      const ids = new Set<typeof ID.Type>()
       for (const definition of definitions) {
         if (ids.has(definition.id)) return yield* Effect.die(new Error(`Duplicate plugin ID: ${definition.id}`))
         ids.add(definition.id)
@@ -56,7 +55,9 @@ const layer = Layer.effect(
           if (
             generation !== undefined &&
             generation.length === definitions.length &&
-            generation.every((id, index) => id === definitions[index]?.id)
+            generation.every(
+              (plugin, index) => plugin.id === definitions[index]?.id && plugin.version === definitions[index]?.version,
+            )
           ) {
             return
           }
@@ -87,7 +88,10 @@ const layer = Layer.effect(
             }),
           )
           if (Exit.isFailure(exit)) return yield* exit
-          generation = definitions.map((definition) => definition.id)
+          generation = definitions.map((definition) => ({
+            id: definition.id,
+            ...(definition.version === undefined ? {} : { version: definition.version }),
+          }))
         }),
       )
     })

+ 37 - 14
packages/core/src/plugin/supervisor.ts

@@ -2,7 +2,7 @@ export * as PluginSupervisor from "./supervisor"
 
 import type { Plugin } from "@opencode-ai/plugin/v2/effect"
 import { Event } from "@opencode-ai/schema/config"
-import { Context, Effect, Fiber, Layer, Schema, Semaphore, Stream } from "effect"
+import { Context, Effect, Fiber, Layer, Option, Schema, Semaphore, Stream } from "effect"
 import path from "path"
 import { fileURLToPath, pathToFileURL } from "url"
 import { Config } from "../config"
@@ -42,6 +42,7 @@ type Operation =
       readonly type: "add"
       readonly target: string
       readonly options: Record<string, unknown>
+      readonly mtime?: number
     }
   | {
       readonly type: "remove"
@@ -57,6 +58,7 @@ type Candidate =
       readonly type: "package"
       readonly specifier: string
       readonly options: Record<string, unknown>
+      readonly mtime?: number
     }
 
 type ConfiguredPackage = {
@@ -94,7 +96,16 @@ const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Con
       }),
     )
   // Explicit config is applied last so it can remove auto-discovered packages.
-  return [...discovered, ...configured]
+  return yield* Effect.forEach([...discovered, ...configured], (operation) => {
+    if (operation.type === "remove" || !path.isAbsolute(operation.target)) return Effect.succeed(operation)
+    return fs.stat(operation.target).pipe(
+      Effect.map((info) => ({
+        ...operation,
+        mtime: Option.getOrElse(info.mtime, () => new Date(0)).getTime(),
+      })),
+      Effect.catch(() => Effect.succeed(operation)),
+    )
+  })
 })
 
 const resolve = Effect.fn("PluginSupervisor.resolve")(function* (
@@ -143,7 +154,16 @@ function apply(pre: readonly Plugin[], post: readonly Plugin[], operations: read
     enabled.has(definition.id) ? [{ type: "definition", definition }] : [],
   )
   const configured: Candidate[] = Array.from(packages.values()).flatMap((item) =>
-    item.enabled ? [{ type: "package", specifier: item.operation.target, options: item.operation.options }] : [],
+    item.enabled
+      ? [
+          {
+            type: "package",
+            specifier: item.operation.target,
+            options: item.operation.options,
+            ...(item.operation.mtime === undefined ? {} : { mtime: item.operation.mtime }),
+          },
+        ]
+      : [],
   )
   const posts: Candidate[] = post.flatMap((definition) =>
     enabled.has(definition.id) ? [{ type: "definition", definition }] : [],
@@ -153,21 +173,29 @@ function apply(pre: readonly Plugin[], post: readonly Plugin[], operations: read
 
 const load = Effect.fn("PluginSupervisor.load")(function* (plan: readonly Candidate[]) {
   return yield* Effect.forEach(plan, (candidate) => {
-    if (candidate.type === "definition") return Effect.succeed(candidate.definition)
+    if (candidate.type === "definition") return Effect.succeed({ plugin: candidate.definition })
     return Effect.gen(function* () {
       const npm = yield* Npm.Service
       const entrypoint = path.isAbsolute(candidate.specifier)
         ? pathToFileURL(candidate.specifier).href
         : (yield* npm.add(candidate.specifier)).entrypoint
       if (!entrypoint) return
-      yield* Effect.log({ msg: "loading plugin", id: candidate.specifier, entrypoint })
-      const mod = yield* Effect.promise(() => import(entrypoint))
+      // Bun currently ignores query parameters when caching file:// imports.
+      const source =
+        candidate.mtime === undefined
+          ? entrypoint
+          : `${candidate.specifier.replaceAll("\\", "/")}?mtime=${candidate.mtime}`
+      yield* Effect.log({ msg: "loading plugin", id: candidate.specifier, entrypoint: source })
+      const mod = yield* Effect.promise(() => import(source))
       const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
       const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
       return {
-        id: plugin.id,
-        effect: (host) => plugin.effect({ ...host, options: candidate.options }),
-      } satisfies Plugin
+        plugin: {
+          id: plugin.id,
+          effect: (host) => plugin.effect({ ...host, options: candidate.options }),
+        } satisfies Plugin,
+        ...(candidate.mtime === undefined ? {} : { version: String(candidate.mtime) }),
+      }
     }).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
   }).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined)))
 })
@@ -231,7 +259,6 @@ const layer = Layer.effect(
     const config = yield* Config.Service
     const events = yield* EventV2.Service
     const lock = Semaphore.makeUnsafe(1)
-    let applied: string | undefined
     const reload = Effect.fn("PluginSupervisor.reload")(() =>
       lock.withPermit(
         Effect.gen(function* () {
@@ -241,15 +268,11 @@ const layer = Layer.effect(
           const pre = [...internal.pre, ...sdk.all()]
           // Read the current layered config before resolving plugin directives and packages.
           const entries = yield* config.entries()
-          // Skip duplicate watcher notifications and config edits unrelated to plugins.
           const operations = yield* scan(entries)
-          const version = JSON.stringify(operations)
-          if (version === applied) return
           // Apply config operations and load enabled package plugins into one ordered generation.
           const plugins = yield* resolve(pre, internal.post, operations)
           // Replace the active generation in one scoped, batched activation.
           yield* registry.activate(plugins)
-          applied = version
         }),
       ),
     )

+ 75 - 2
packages/core/test/config/plugin.test.ts

@@ -1,7 +1,10 @@
 import fs from "fs/promises"
 import path from "path"
+import { pathToFileURL } from "url"
 import { describe, expect } from "bun:test"
 import { define } from "@opencode-ai/plugin/v2/effect"
+import { Config as ConfigSchema } from "@opencode-ai/schema/config"
+import { Plugin } from "@opencode-ai/schema/plugin"
 import { AgentV2 } from "@opencode-ai/core/agent"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -33,7 +36,7 @@ describe("PluginSupervisor config", () => {
         yield* ready()
         expect(
           (yield* plugins.list()).map((plugin) => plugin.id).filter((id) => id.startsWith("opencode.provider.")),
-        ).toEqual([PluginV2.ID.make("opencode.provider.openai")])
+        ).toEqual([Plugin.ID.make("opencode.provider.openai")])
       }),
     ),
   )
@@ -122,6 +125,43 @@ describe("PluginSupervisor config", () => {
     ),
   )
 
+  it.live("reloads an auto-discovered plugin when its file changes", () =>
+    withLocation(
+      undefined,
+      Effect.gen(function* () {
+        yield* ready()
+        const agents = yield* AgentV2.Service
+        const events = yield* EventV2.Service
+        const location = yield* Location.Service
+        const plugins = yield* PluginV2.Service
+        const file = path.join(location.directory, ".opencode", "plugin", "mutable.ts")
+        const first = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id
+
+        expect(first).toBeDefined()
+        expect((yield* agents.get(AgentV2.ID.make("mutable")))?.description).toBe("first")
+
+        yield* Effect.promise(async () => {
+          await fs.writeFile(file, mutablePlugin("second"))
+          const modified = new Date(Date.now() + 5_000)
+          await fs.utimes(file, modified, modified)
+        })
+        yield* events.publish(ConfigSchema.Event.Updated, {})
+        yield* waitUntil(
+          Effect.gen(function* () {
+            const current = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id
+            return current === first && (yield* agents.get(AgentV2.ID.make("mutable")))?.description === "second"
+          }),
+        )
+      }),
+      false,
+      async (directory) => {
+        const plugin = path.join(directory, ".opencode", "plugin")
+        await fs.mkdir(plugin, { recursive: true })
+        await fs.writeFile(path.join(plugin, "mutable.ts"), mutablePlugin("first"))
+      },
+    ),
+  )
+
   it.live("applies explicit removals after auto-discovery", () =>
     withLocation(
       { plugins: ["-*"] },
@@ -192,13 +232,19 @@ const ready = Effect.fnUntraced(function* () {
   yield* supervisor.ready
 })
 
-function withLocation<A, E, R>(config: unknown, effect: Effect.Effect<A, E, R>, fixtures = false) {
+function withLocation<A, E, R>(
+  config: unknown,
+  effect: Effect.Effect<A, E, R>,
+  fixtures = false,
+  prepare?: (directory: string) => Promise<void>,
+) {
   return Effect.acquireRelease(
     Effect.promise(() => tmpdir()),
     (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
   ).pipe(
     Effect.tap((tmp) =>
       Effect.promise(async () => {
+        await prepare?.(tmp.path)
         if (fixtures) {
           const directory = path.join(tmp.path, ".opencode")
           await fs.mkdir(directory, { recursive: true })
@@ -223,3 +269,30 @@ function withLocation<A, E, R>(config: unknown, effect: Effect.Effect<A, E, R>,
     ),
   )
 }
+
+function mutablePlugin(description: string) {
+  const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/v2/promise/index.ts")).href
+  return `
+import { define } from ${JSON.stringify(plugin)}
+
+export default define({
+  id: "mutable-plugin",
+  setup: async (ctx) => {
+    await ctx.agent.transform((agents) => {
+      agents.update("mutable", (agent) => {
+        agent.description = ${JSON.stringify(description)}
+        agent.mode = "subagent"
+      })
+    })
+  },
+})
+`
+}
+
+const waitUntil = Effect.fnUntraced(function* (condition: Effect.Effect<boolean>) {
+  for (let attempt = 0; attempt < 200; attempt++) {
+    if (yield* condition) return
+    yield* Effect.sleep("10 millis")
+  }
+  return yield* Effect.die("Timed out waiting for plugin reload")
+})

+ 3 - 2
packages/core/test/location-layer.test.ts

@@ -2,6 +2,7 @@ import fs from "fs/promises"
 import path from "path"
 import { describe, expect } from "bun:test"
 import { Config } from "@opencode-ai/schema/config"
+import { Plugin } from "@opencode-ai/schema/plugin"
 import { Context, DateTime, Effect, Equal, Hash, Schema, Stream } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect"
 import { AgentV2 } from "@opencode-ai/core/agent"
@@ -50,7 +51,7 @@ describe("LocationServiceMap", () => {
             ),
           )
 
-          expect(plugins.map((plugin) => plugin.id)).toEqual([PluginV2.ID.make("opencode.agent")])
+          expect(plugins.map((plugin) => plugin.id)).toEqual([Plugin.ID.make("opencode.agent")])
         }),
       ),
     ),
@@ -377,7 +378,7 @@ describe("LocationServiceMap", () => {
                 })
                 .pipe(Effect.asVoid),
           })
-          yield* plugins.activate([{ id: PluginV2.ID.make(reviewer.id), effect: reviewer.effect }])
+          yield* plugins.activate([{ plugin: reviewer }])
 
           expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
             description: "Reviews code",

+ 23 - 22
packages/core/test/plugin.test.ts

@@ -2,6 +2,7 @@ import { describe, expect } from "bun:test"
 import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect"
 import { define } from "@opencode-ai/plugin/v2/effect"
 import { Config as ConfigSchema } from "@opencode-ai/schema/config"
+import { Plugin } from "@opencode-ai/schema/plugin"
 import { AgentV2 } from "@opencode-ai/core/agent"
 import { EventV2 } from "@opencode-ai/core/event"
 import { PluginV2 } from "@opencode-ai/core/plugin"
@@ -37,15 +38,15 @@ describe("PluginV2", () => {
     }),
   )
 
-  it.effect("skips identical generations and replaces changed plugin IDs", () =>
+  it.effect("skips identical generations and replaces changed plugin versions", () =>
     Effect.gen(function* () {
       const plugins = yield* PluginV2.Service
       const agents = yield* AgentV2.Service
       let description = "first"
 
-      const managed = (id: string) =>
+      const managed = () =>
         define({
-          id,
+          id: "managed",
           effect: (ctx) =>
             ctx.agent
               .transform((agents) =>
@@ -56,15 +57,15 @@ describe("PluginV2", () => {
               .pipe(Effect.asVoid),
         })
 
-      yield* plugins.activate([managed("managed")])
+      yield* plugins.activate([{ plugin: managed() }])
 
       expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
 
       description = "second"
-      yield* plugins.activate([managed("managed")])
+      yield* plugins.activate([{ plugin: managed() }])
       expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
 
-      yield* plugins.activate([managed("managed-next")])
+      yield* plugins.activate([{ plugin: managed(), version: "next" }])
       expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
 
       yield* plugins.activate([])
@@ -75,14 +76,14 @@ describe("PluginV2", () => {
   it.effect("rejects duplicate IDs before replacing the active generation", () =>
     Effect.gen(function* () {
       const plugins = yield* PluginV2.Service
-      const active = PluginV2.ID.make("active")
-      const duplicate = PluginV2.ID.make("duplicate")
-      yield* plugins.activate([{ id: active, effect: () => Effect.void }])
+      const active = Plugin.ID.make("active")
+      const duplicate = "duplicate"
+      yield* plugins.activate([{ plugin: { id: active, effect: () => Effect.void } }])
 
       const result = yield* plugins
         .activate([
-          { id: duplicate, effect: () => Effect.void },
-          { id: duplicate, effect: () => Effect.void },
+          { plugin: { id: duplicate, effect: () => Effect.void } },
+          { plugin: { id: duplicate, effect: () => Effect.void } },
         ])
         .pipe(Effect.exit)
 
@@ -105,11 +106,11 @@ describe("PluginV2", () => {
             .pipe(Effect.asVoid),
       })
 
-      expect(Exit.isFailure(yield* plugins.activate([plugin]).pipe(Effect.exit))).toBe(true)
+      expect(Exit.isFailure(yield* plugins.activate([{ plugin }]).pipe(Effect.exit))).toBe(true)
       fail = false
-      yield* plugins.activate([plugin])
+      yield* plugins.activate([{ plugin }])
 
-      expect(yield* plugins.list()).toEqual([{ id: PluginV2.ID.make("retry") }])
+      expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("retry") }])
     }),
   )
 
@@ -119,8 +120,10 @@ describe("PluginV2", () => {
       const closed: string[] = []
       yield* plugins.activate(
         ["first", "second"].map((id) => ({
-          id: PluginV2.ID.make(id),
-          effect: () => Effect.addFinalizer(() => Effect.sync(() => closed.push(id))),
+          plugin: {
+            id,
+            effect: () => Effect.addFinalizer(() => Effect.sync(() => closed.push(id))),
+          },
         })),
       )
 
@@ -143,9 +146,7 @@ describe("PluginV2", () => {
           ),
       })
 
-      yield* plugins
-        .activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }])
-        .pipe(Effect.provideService(Secret, "secret"))
+      yield* plugins.activate([{ plugin }]).pipe(Effect.provideService(Secret, "secret"))
 
       expect(visible).toBe(false)
     }),
@@ -170,7 +171,7 @@ describe("PluginV2", () => {
             .pipe(Effect.orDie),
       })
 
-      yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }])
+      yield* plugins.activate([{ plugin }])
       expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toContain(
         "plugin_tool",
       )
@@ -205,7 +206,7 @@ describe("PluginV2", () => {
           }),
       })
 
-      yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }])
+      yield* plugins.activate([{ plugin }])
 
       expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toEqual([
         "plain",
@@ -257,7 +258,7 @@ describe("PluginV2", () => {
           }),
       })
 
-      yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }])
+      yield* plugins.activate([{ plugin }])
 
       const materialized = yield* registry.materialize({ model: testModel })
       const settlement = yield* materialized.settle({

+ 1 - 3
packages/core/test/plugin/provider-kilo.test.ts

@@ -19,9 +19,7 @@ const addPlugin = Effect.fn(function* () {
 
 describe("KiloPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
-    Effect.sync(() =>
-      expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.kilo")),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.kilo")),
   )
 
   it.effect("applies legacy referer headers only to kilo", () =>

+ 1 - 3
packages/core/test/plugin/provider-llmgateway.test.ts

@@ -21,9 +21,7 @@ const addPlugin = Effect.fn(function* () {
 
 describe("LLMGatewayPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
-    Effect.sync(() =>
-      expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.llmgateway")),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.llmgateway")),
   )
 
   it.effect("applies legacy referer headers only to enabled llmgateway", () =>

+ 1 - 3
packages/core/test/plugin/provider-nvidia.test.ts

@@ -19,9 +19,7 @@ const addPlugin = Effect.fn(function* () {
 
 describe("NvidiaPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
-    Effect.sync(() =>
-      expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.nvidia")),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.nvidia")),
   )
 
   it.effect("applies NVIDIA tracking headers only to nvidia", () =>

+ 1 - 3
packages/core/test/plugin/provider-openrouter.test.ts

@@ -22,9 +22,7 @@ const addPlugin = Effect.fn(function* () {
 
 describe("OpenRouterPlugin", () => {
   it.effect("is registered so legacy OpenRouter behavior can be applied", () =>
-    Effect.sync(() =>
-      expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.openrouter")),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.openrouter")),
   )
 
   it.effect("applies legacy referer headers only to openrouter", () =>

+ 1 - 1
packages/core/test/plugin/provider-snowflake-cortex.test.ts

@@ -43,7 +43,7 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
 describe("SnowflakeCortexPlugin", () => {
   it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
     Effect.sync(() => {
-      expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.snowflake-cortex"))
+      expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.snowflake-cortex")
       const ids = ProviderPlugins.map((p) => p.id)
       expect(ids.indexOf("opencode.provider.snowflake-cortex")).toBeLessThan(
         ids.indexOf("opencode.provider.openai-compatible"),

+ 1 - 3
packages/core/test/plugin/provider-zenmux.test.ts

@@ -24,9 +24,7 @@ function required<T>(value: T | undefined): T {
 
 describe("ZenmuxPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
-    Effect.sync(() =>
-      expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.zenmux")),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.zenmux")),
   )
 
   it.effect("applies the exact legacy Zenmux headers", () =>

+ 0 - 4
packages/core/test/shared-schema.test.ts

@@ -22,14 +22,12 @@ import { FileSystem } from "@opencode-ai/schema/filesystem"
 import { Integration } from "@opencode-ai/schema/integration"
 import { LLM } from "@opencode-ai/schema/llm"
 import { Permission } from "@opencode-ai/schema/permission"
-import { Plugin } from "@opencode-ai/schema/plugin"
 import { Pty } from "@opencode-ai/schema/pty"
 import { Reference } from "@opencode-ai/schema/reference"
 import { SessionTodo } from "@opencode-ai/schema/session-todo"
 import { Skill } from "@opencode-ai/schema/skill"
 import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { PluginV2 } from "@opencode-ai/core/plugin"
 
 test("Core reuses the canonical shared schemas", async () => {
   const [
@@ -129,8 +127,6 @@ test("Core reuses the canonical shared schemas", async () => {
     [corePermission.Ruleset, Permission.Ruleset],
     [corePermissionV1.Event, PermissionV1.Event],
     [coreProjectCopy.Event, ProjectDirectories.Event],
-    [PluginV2.ID, Plugin.ID],
-    [PluginV2.Event, Plugin.Event],
     [corePty.Info, Pty.Info],
     [corePty.Event, Pty.Event],
     [coreProject.ID, Project.ID],