Jelajahi Sumber

Add Effect-native core event system (#27415)

Dax 3 bulan lalu
induk
melakukan
e11e089e42
43 mengubah file dengan 1506 tambahan dan 523 penghapusan
  1. 17 8
      packages/core/src/catalog.ts
  2. 157 0
      packages/core/src/event.ts
  3. 0 12
      packages/core/src/instance-layer.ts
  4. 0 10
      packages/core/src/instance.ts
  5. 12 0
      packages/core/src/location-layer.ts
  6. 11 0
      packages/core/src/location.ts
  7. 70 75
      packages/core/src/session-event.ts
  8. 0 0
      packages/core/src/session-message-updater.ts
  9. 12 12
      packages/core/src/session-message.ts
  10. 13 0
      packages/core/src/session.ts
  11. 36 4
      packages/core/test/catalog.test.ts
  12. 133 0
      packages/core/test/event.test.ts
  13. 3 3
      packages/core/test/plugin/provider-opencode.test.ts
  14. 23 10
      packages/opencode/src/bus/bus-event.ts
  15. 3 3
      packages/opencode/src/cli/cmd/debug/v2.ts
  16. 2 0
      packages/opencode/src/effect/app-runtime.ts
  17. 99 0
      packages/opencode/src/event-v2-bridge.ts
  18. 1 0
      packages/opencode/src/server/routes/instance/httpapi/groups/global.ts
  19. 18 18
      packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts
  20. 1 1
      packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts
  21. 4 4
      packages/opencode/src/server/routes/instance/httpapi/groups/v2/model.ts
  22. 6 6
      packages/opencode/src/server/routes/instance/httpapi/groups/v2/provider.ts
  23. 1 1
      packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts
  24. 2 2
      packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts
  25. 1 1
      packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts
  26. 2 0
      packages/opencode/src/server/routes/instance/httpapi/server.ts
  27. 8 19
      packages/opencode/src/session/compaction.ts
  28. 19 34
      packages/opencode/src/session/processor.ts
  29. 29 28
      packages/opencode/src/session/projectors-next.ts
  30. 12 12
      packages/opencode/src/session/prompt.ts
  31. 2 7
      packages/opencode/src/session/schema.ts
  32. 1 1
      packages/opencode/src/session/session.sql.ts
  33. 47 16
      packages/opencode/src/sync/index.ts
  34. 0 43
      packages/opencode/src/v2/event.ts
  35. 8 8
      packages/opencode/src/v2/session.ts
  36. 1 1
      packages/opencode/test/server/httpapi-session.test.ts
  37. 3 0
      packages/opencode/test/session/compaction.test.ts
  38. 2 0
      packages/opencode/test/session/processor-effect.test.ts
  39. 2 0
      packages/opencode/test/session/prompt.test.ts
  40. 2 0
      packages/opencode/test/session/snapshot-tool-race.test.ts
  41. 3 3
      packages/opencode/test/v2/session-message-updater.test.ts
  42. 6 6
      packages/sdk/js/src/v2/gen/sdk.gen.ts
  43. 734 175
      packages/sdk/js/src/v2/gen/types.gen.ts

+ 17 - 8
packages/core/src/catalog.ts

@@ -5,7 +5,8 @@ import { produce, type Draft } from "immer"
 import { ModelV2 } from "./model"
 import { PluginV2 } from "./plugin"
 import { ProviderV2 } from "./provider"
-import { Instance } from "./instance"
+import { Location } from "./location"
+import { EventV2 } from "./event"
 
 type ProviderRecord = {
   provider: ProviderV2.Info
@@ -24,6 +25,15 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundErr
   modelID: ModelV2.ID,
 }) {}
 
+export const Event = {
+  ModelUpdated: EventV2.define({
+    type: "catalog.model.updated",
+    schema: {
+      model: ModelV2.Info,
+    },
+  }),
+}
+
 export interface Interface {
   readonly provider: {
     readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
@@ -57,10 +67,11 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
 export const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
-    yield* Instance.Service
+    yield* Location.Service
     let records = HashMap.empty<ProviderV2.ID, ProviderRecord>()
     let defaultModel: { providerID: ProviderV2.ID; modelID: ModelV2.ID } | undefined
     const plugin = yield* PluginV2.Service
+    const events = yield* EventV2.Service
 
     const resolve = (model: ModelV2.Info) => {
       const provider = Option.getOrThrow(HashMap.get(records, model.providerID)).provider
@@ -157,14 +168,12 @@ export const layer = Layer.effect(
           )
           const updated = yield* plugin.trigger("model.update", {}, { model, cancel: false })
           if (updated.cancel) return
+          const next = new ModelV2.Info({ ...updated.model, id: modelID, providerID })
           records = HashMap.set(records, providerID, {
             provider: record.provider,
-            models: HashMap.set(
-              record.models,
-              modelID,
-              new ModelV2.Info({ ...updated.model, id: modelID, providerID }),
-            ),
+            models: HashMap.set(record.models, modelID, next),
           })
+          yield* events.publish(Event.ModelUpdated, { model: resolve(next) })
           return
         }),
 
@@ -257,4 +266,4 @@ export const layer = Layer.effect(
 
 const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
 
-export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer))
+export const defaultLayer = layer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provide(PluginV2.defaultLayer))

+ 157 - 0
packages/core/src/event.ts

@@ -0,0 +1,157 @@
+import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
+import { Location } from "./location"
+import { withStatics } from "./schema"
+import { Identifier } from "./util/identifier"
+
+export const ID = Schema.String.pipe(
+  Schema.brand("Event.ID"),
+  withStatics((schema) => ({ create: () => schema.make("evt_" + Identifier.ascending()) })),
+)
+export type ID = typeof ID.Type
+
+export type Definition<Type extends string = string, DataSchema extends Schema.Top = Schema.Top> = {
+  readonly type: Type
+  readonly version?: number
+  readonly aggregate?: string
+  readonly data: DataSchema
+}
+
+export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
+
+export type Payload<D extends Definition = Definition> = {
+  readonly id: ID
+  readonly type: D["type"]
+  readonly data: Data<D>
+  readonly version?: number
+  readonly location?: Location.Ref
+  readonly metadata?: Record<string, unknown>
+}
+
+export type Sync = (event: Payload) => Effect.Effect<void>
+
+export const registry = new Map<string, Definition>()
+
+export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
+  readonly type: Type
+  readonly version?: number
+  readonly aggregate?: string
+  readonly schema: Fields
+}): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
+  const Data = Schema.Struct(input.schema)
+  const Payload = Schema.Struct({
+    id: ID,
+    metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
+    type: Schema.Literal(input.type),
+    version: Schema.optional(Schema.Number),
+    location: Schema.optional(Location.Ref),
+    data: Data,
+  }).annotate({ identifier: input.type })
+
+  const definition = Object.assign(Payload, {
+    type: input.type,
+    ...(input.version === undefined ? {} : { version: input.version }),
+    ...(input.aggregate === undefined ? {} : { aggregate: input.aggregate }),
+    data: Data,
+  })
+  registry.set(input.type, definition)
+  return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
+    Definition<Type, Schema.Struct<Fields>>
+}
+
+export function definitions() {
+  return registry.values().toArray()
+}
+
+export interface PublishOptions {
+  readonly id?: ID
+  readonly metadata?: Record<string, unknown>
+}
+
+export type Unsubscribe = Effect.Effect<void>
+
+export interface Interface {
+  readonly publish: <D extends Definition>(
+    definition: D,
+    data: Data<D>,
+    options?: PublishOptions,
+  ) => Effect.Effect<Payload<D>>
+  readonly publishEvent: <D extends Definition>(event: Payload<D>) => Effect.Effect<Payload<D>>
+  readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
+  readonly all: () => Stream.Stream<Payload>
+  readonly sync: (handler: Sync) => Effect.Effect<Unsubscribe>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/Event") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const all = yield* PubSub.unbounded<Payload>()
+    const typed = new Map<string, PubSub.PubSub<Payload>>()
+    const syncHandlers = new Array<Sync>()
+
+    const getOrCreate = (definition: Definition) =>
+      Effect.gen(function* () {
+        const existing = typed.get(definition.type)
+        if (existing) return existing
+        const pubsub = yield* PubSub.unbounded<Payload>()
+        typed.set(definition.type, pubsub)
+        return pubsub
+      })
+
+    yield* Effect.addFinalizer(() =>
+      Effect.gen(function* () {
+        yield* PubSub.shutdown(all)
+        yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true })
+      }),
+    )
+
+    function publishEvent<D extends Definition>(event: Payload<D>) {
+      return Effect.gen(function* () {
+        for (const sync of syncHandlers) {
+          yield* sync(event as Payload)
+        }
+        const pubsub = typed.get(event.type)
+        if (pubsub) yield* PubSub.publish(pubsub, event as Payload)
+        yield* PubSub.publish(all, event as Payload)
+        return event
+      })
+    }
+
+    function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
+      return Effect.gen(function* () {
+        const location = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
+        const event = {
+          id: options?.id ?? ID.create(),
+          ...(options?.metadata ? { metadata: options.metadata } : {}),
+          type: definition.type,
+          ...(definition.version === undefined ? {} : { version: definition.version }),
+          ...(location ? { location } : {}),
+          data,
+        } as Payload<D>
+        return yield* publishEvent(event)
+      })
+    }
+
+    const subscribe = <D extends Definition>(definition: D): Stream.Stream<Payload<D>> =>
+      Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe(
+        Stream.map((event) => event as Payload<D>),
+      )
+
+    const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(all)
+    const sync = (handler: Sync): Effect.Effect<Unsubscribe> =>
+      Effect.sync(() => {
+        syncHandlers.push(handler)
+        return Effect.sync(() => {
+          const index = syncHandlers.indexOf(handler)
+          if (index >= 0) syncHandlers.splice(index, 1)
+        })
+      })
+
+    return Service.of({ publish, publishEvent, subscribe, all: streamAll, sync })
+  }),
+)
+
+export const defaultLayer = layer
+
+export * as EventV2 from "./event"

+ 0 - 12
packages/core/src/instance-layer.ts

@@ -1,12 +0,0 @@
-import { Layer, LayerMap } from "effect"
-import { Instance } from "./instance"
-import { Catalog } from "./catalog"
-import { PluginBoot } from "./plugin/boot"
-
-export class InstanceServiceMap extends LayerMap.Service<InstanceServiceMap>()("@opencode/example/InstanceServiceMap", {
-  lookup: (ref: Instance.Ref) => {
-    const instance = Layer.succeed(Instance.Service, Instance.Service.of(ref))
-    return Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer).pipe(Layer.provide(instance))
-  },
-  idleTimeToLive: "5 minutes",
-}) {}

+ 0 - 10
packages/core/src/instance.ts

@@ -1,10 +0,0 @@
-import { Context } from "effect"
-
-export * as Instance from "./instance"
-
-export type Ref = {
-  readonly directory: string
-  readonly workspaceID?: string
-}
-
-export class Service extends Context.Service<Service, Ref>()("@opencode/Instance") {}

+ 12 - 0
packages/core/src/location-layer.ts

@@ -0,0 +1,12 @@
+import { Layer, LayerMap } from "effect"
+import { Location } from "./location"
+import { Catalog } from "./catalog"
+import { PluginBoot } from "./plugin/boot"
+
+export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
+  lookup: (ref: Location.Ref) => {
+    const location = Layer.succeed(Location.Service, Location.Service.of(ref))
+    return Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer).pipe(Layer.provide(location))
+  },
+  idleTimeToLive: "5 minutes",
+}) {}

+ 11 - 0
packages/core/src/location.ts

@@ -0,0 +1,11 @@
+import { Context, Schema } from "effect"
+
+export * as Location from "./location"
+
+export const Ref = Schema.Struct({
+  directory: Schema.String,
+  workspaceID: Schema.optional(Schema.String),
+}).annotate({ identifier: "Location.Ref" })
+export type Ref = typeof Ref.Type
+
+export class Service extends Context.Service<Service, Ref>()("@opencode/Location") {}

+ 70 - 75
packages/opencode/src/v2/session-event.ts → packages/core/src/session-event.ts

@@ -1,12 +1,13 @@
-import { SessionID } from "@/session/schema"
-import { NonNegativeInt } from "@opencode-ai/core/schema"
-import { EventV2 } from "./event"
-import { FileAttachment, Prompt } from "@opencode-ai/core/session-prompt"
 import { Schema } from "effect"
+import { EventV2 } from "./event"
+import { ModelV2 } from "./model"
+import { NonNegativeInt } from "./schema"
+import { Session } from "./session"
+import { FileAttachment, Prompt } from "./session-prompt"
+import { ToolOutput } from "./tool-output"
+import { V2Schema } from "./v2-schema"
+
 export { FileAttachment }
-import { ToolOutput } from "@opencode-ai/core/tool-output"
-import { V2Schema } from "@opencode-ai/core/v2-schema"
-import { ModelV2 } from "@opencode-ai/core/model"
 
 export const Source = Schema.Struct({
   start: NonNegativeInt,
@@ -15,92 +16,94 @@ export const Source = Schema.Struct({
 }).annotate({
   identifier: "session.next.event.source",
 })
-export type Source = Schema.Schema.Type<typeof Source>
+export type Source = typeof Source.Type
 
 const Base = {
   timestamp: V2Schema.DateTimeUtcFromMillis,
-  sessionID: SessionID,
+  sessionID: Session.ID,
 }
 
+const options = {
+  aggregate: "sessionID",
+  version: 1,
+} as const
+
 export const UnknownError = Schema.Struct({
   type: Schema.Literal("unknown"),
   message: Schema.String,
 }).annotate({
   identifier: "Session.Error.Unknown",
 })
-export type UnknownError = Schema.Schema.Type<typeof UnknownError>
+export type UnknownError = typeof UnknownError.Type
 
 export const AgentSwitched = EventV2.define({
   type: "session.next.agent.switched",
-  aggregate: "sessionID",
-  version: 1,
+  ...options,
   schema: {
     ...Base,
     agent: Schema.String,
   },
 })
-export type AgentSwitched = Schema.Schema.Type<typeof AgentSwitched>
+export type AgentSwitched = typeof AgentSwitched.Type
 
 export const ModelSwitched = EventV2.define({
   type: "session.next.model.switched",
-  aggregate: "sessionID",
-  version: 1,
+  ...options,
   schema: {
     ...Base,
     model: ModelV2.Ref,
   },
 })
-export type ModelSwitched = Schema.Schema.Type<typeof ModelSwitched>
+export type ModelSwitched = typeof ModelSwitched.Type
 
 export const Prompted = EventV2.define({
   type: "session.next.prompted",
-  aggregate: "sessionID",
-  version: 1,
+  ...options,
   schema: {
     ...Base,
     prompt: Prompt,
   },
 })
-export type Prompted = Schema.Schema.Type<typeof Prompted>
+export type Prompted = typeof Prompted.Type
 
 export const Synthetic = EventV2.define({
   type: "session.next.synthetic",
-  aggregate: "sessionID",
+  ...options,
   schema: {
     ...Base,
     text: Schema.String,
   },
 })
-export type Synthetic = Schema.Schema.Type<typeof Synthetic>
+export type Synthetic = typeof Synthetic.Type
 
 export namespace Shell {
   export const Started = EventV2.define({
     type: "session.next.shell.started",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       callID: Schema.String,
       command: Schema.String,
     },
   })
-  export type Started = Schema.Schema.Type<typeof Started>
+  export type Started = typeof Started.Type
 
   export const Ended = EventV2.define({
     type: "session.next.shell.ended",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       callID: Schema.String,
       output: Schema.String,
     },
   })
-  export type Ended = Schema.Schema.Type<typeof Ended>
+  export type Ended = typeof Ended.Type
 }
 
 export namespace Step {
   export const Started = EventV2.define({
     type: "session.next.step.started",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       agent: Schema.String,
@@ -108,11 +111,11 @@ export namespace Step {
       snapshot: Schema.String.pipe(Schema.optional),
     },
   })
-  export type Started = Schema.Schema.Type<typeof Started>
+  export type Started = typeof Started.Type
 
   export const Ended = EventV2.define({
     type: "session.next.step.ended",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       finish: Schema.String,
@@ -129,123 +132,123 @@ export namespace Step {
       snapshot: Schema.String.pipe(Schema.optional),
     },
   })
-  export type Ended = Schema.Schema.Type<typeof Ended>
+  export type Ended = typeof Ended.Type
 
   export const Failed = EventV2.define({
     type: "session.next.step.failed",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       error: UnknownError,
     },
   })
-  export type Failed = Schema.Schema.Type<typeof Failed>
+  export type Failed = typeof Failed.Type
 }
 
 export namespace Text {
   export const Started = EventV2.define({
     type: "session.next.text.started",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
     },
   })
-  export type Started = Schema.Schema.Type<typeof Started>
+  export type Started = typeof Started.Type
 
   export const Delta = EventV2.define({
     type: "session.next.text.delta",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       delta: Schema.String,
     },
   })
-  export type Delta = Schema.Schema.Type<typeof Delta>
+  export type Delta = typeof Delta.Type
 
   export const Ended = EventV2.define({
     type: "session.next.text.ended",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       text: Schema.String,
     },
   })
-  export type Ended = Schema.Schema.Type<typeof Ended>
+  export type Ended = typeof Ended.Type
 }
 
 export namespace Reasoning {
   export const Started = EventV2.define({
     type: "session.next.reasoning.started",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       reasoningID: Schema.String,
     },
   })
-  export type Started = Schema.Schema.Type<typeof Started>
+  export type Started = typeof Started.Type
 
   export const Delta = EventV2.define({
     type: "session.next.reasoning.delta",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       reasoningID: Schema.String,
       delta: Schema.String,
     },
   })
-  export type Delta = Schema.Schema.Type<typeof Delta>
+  export type Delta = typeof Delta.Type
 
   export const Ended = EventV2.define({
     type: "session.next.reasoning.ended",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       reasoningID: Schema.String,
       text: Schema.String,
     },
   })
-  export type Ended = Schema.Schema.Type<typeof Ended>
+  export type Ended = typeof Ended.Type
 }
 
 export namespace Tool {
   export namespace Input {
     export const Started = EventV2.define({
       type: "session.next.tool.input.started",
-      aggregate: "sessionID",
+      ...options,
       schema: {
         ...Base,
         callID: Schema.String,
         name: Schema.String,
       },
     })
-    export type Started = Schema.Schema.Type<typeof Started>
+    export type Started = typeof Started.Type
 
     export const Delta = EventV2.define({
       type: "session.next.tool.input.delta",
-      aggregate: "sessionID",
+      ...options,
       schema: {
         ...Base,
         callID: Schema.String,
         delta: Schema.String,
       },
     })
-    export type Delta = Schema.Schema.Type<typeof Delta>
+    export type Delta = typeof Delta.Type
 
     export const Ended = EventV2.define({
       type: "session.next.tool.input.ended",
-      aggregate: "sessionID",
+      ...options,
       schema: {
         ...Base,
         callID: Schema.String,
         text: Schema.String,
       },
     })
-    export type Ended = Schema.Schema.Type<typeof Ended>
+    export type Ended = typeof Ended.Type
   }
 
   export const Called = EventV2.define({
     type: "session.next.tool.called",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       callID: Schema.String,
@@ -257,11 +260,11 @@ export namespace Tool {
       }),
     },
   })
-  export type Called = Schema.Schema.Type<typeof Called>
+  export type Called = typeof Called.Type
 
   export const Progress = EventV2.define({
     type: "session.next.tool.progress",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       callID: Schema.String,
@@ -269,11 +272,11 @@ export namespace Tool {
       content: Schema.Array(ToolOutput.Content),
     },
   })
-  export type Progress = Schema.Schema.Type<typeof Progress>
+  export type Progress = typeof Progress.Type
 
   export const Success = EventV2.define({
     type: "session.next.tool.success",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       callID: Schema.String,
@@ -285,11 +288,11 @@ export namespace Tool {
       }),
     },
   })
-  export type Success = Schema.Schema.Type<typeof Success>
+  export type Success = typeof Success.Type
 
   export const Failed = EventV2.define({
     type: "session.next.tool.failed",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       callID: Schema.String,
@@ -300,7 +303,7 @@ export namespace Tool {
       }),
     },
   })
-  export type Failed = Schema.Schema.Type<typeof Failed>
+  export type Failed = typeof Failed.Type
 }
 
 export const RetryError = Schema.Struct({
@@ -313,49 +316,50 @@ export const RetryError = Schema.Struct({
 }).annotate({
   identifier: "session.next.retry_error",
 })
-export type RetryError = Schema.Schema.Type<typeof RetryError>
+export type RetryError = typeof RetryError.Type
 
 export const Retried = EventV2.define({
   type: "session.next.retried",
-  aggregate: "sessionID",
+  ...options,
   schema: {
     ...Base,
     attempt: Schema.Finite,
     error: RetryError,
   },
 })
-export type Retried = Schema.Schema.Type<typeof Retried>
+export type Retried = typeof Retried.Type
 
 export namespace Compaction {
   export const Started = EventV2.define({
     type: "session.next.compaction.started",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
     },
   })
-  export type Started = Schema.Schema.Type<typeof Started>
+  export type Started = typeof Started.Type
 
   export const Delta = EventV2.define({
     type: "session.next.compaction.delta",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       text: Schema.String,
     },
   })
+  export type Delta = typeof Delta.Type
 
   export const Ended = EventV2.define({
     type: "session.next.compaction.ended",
-    aggregate: "sessionID",
+    ...options,
     schema: {
       ...Base,
       text: Schema.String,
       include: Schema.String.pipe(Schema.optional),
     },
   })
-  export type Ended = Schema.Schema.Type<typeof Ended>
+  export type Ended = typeof Ended.Type
 }
 
 export const All = Schema.Union(
@@ -392,16 +396,7 @@ export const All = Schema.Union(
   },
 ).pipe(Schema.toTaggedUnion("type"))
 
-// user
-// assistant
-// assistant
-// assistant
-// user
-// compaction marker
-// -> text
-// assistant
-
-export type Event = Schema.Schema.Type<typeof All>
+export type Event = typeof All.Type
 export type Type = Event["type"]
 
 export * as SessionEvent from "./session-event"

+ 0 - 0
packages/opencode/src/v2/session-message-updater.ts → packages/core/src/session-message-updater.ts


+ 12 - 12
packages/opencode/src/v2/session-message.ts → packages/core/src/session-message.ts

@@ -1,10 +1,10 @@
 import { Schema } from "effect"
-import { Prompt } from "@opencode-ai/core/session-prompt"
+import { Prompt } from "./session-prompt"
 import { SessionEvent } from "./session-event"
 import { EventV2 } from "./event"
-import { ToolOutput } from "@opencode-ai/core/tool-output"
-import { V2Schema } from "@opencode-ai/core/v2-schema"
-import { ModelV2 } from "@opencode-ai/core/model"
+import { ToolOutput } from "./tool-output"
+import { V2Schema } from "./v2-schema"
+import { ModelV2 } from "./model"
 
 export const ID = EventV2.ID
 export type ID = Schema.Schema.Type<typeof ID>
@@ -20,7 +20,7 @@ const Base = {
 export class AgentSwitched extends Schema.Class<AgentSwitched>("Session.Message.AgentSwitched")({
   ...Base,
   type: Schema.Literal("agent-switched"),
-  agent: SessionEvent.AgentSwitched.fields.data.fields.agent,
+  agent: SessionEvent.AgentSwitched.data.fields.agent,
 }) {}
 
 export class ModelSwitched extends Schema.Class<ModelSwitched>("Session.Message.ModelSwitched")({
@@ -43,16 +43,16 @@ export class User extends Schema.Class<User>("Session.Message.User")({
 
 export class Synthetic extends Schema.Class<Synthetic>("Session.Message.Synthetic")({
   ...Base,
-  sessionID: SessionEvent.Synthetic.fields.data.fields.sessionID,
-  text: SessionEvent.Synthetic.fields.data.fields.text,
+  sessionID: SessionEvent.Synthetic.data.fields.sessionID,
+  text: SessionEvent.Synthetic.data.fields.text,
   type: Schema.Literal("synthetic"),
 }) {}
 
 export class Shell extends Schema.Class<Shell>("Session.Message.Shell")({
   ...Base,
   type: Schema.Literal("shell"),
-  callID: SessionEvent.Shell.Started.fields.data.fields.callID,
-  command: SessionEvent.Shell.Started.fields.data.fields.command,
+  callID: SessionEvent.Shell.Started.data.fields.callID,
+  command: SessionEvent.Shell.Started.data.fields.command,
   output: Schema.String,
   time: Schema.Struct({
     created: V2Schema.DateTimeUtcFromMillis,
@@ -130,7 +130,7 @@ export class Assistant extends Schema.Class<Assistant>("Session.Message.Assistan
   ...Base,
   type: Schema.Literal("assistant"),
   agent: Schema.String,
-  model: SessionEvent.Step.Started.fields.data.fields.model,
+  model: SessionEvent.Step.Started.data.fields.model,
   content: AssistantContent.pipe(Schema.Array),
   snapshot: Schema.Struct({
     start: Schema.String.pipe(Schema.optional),
@@ -147,7 +147,7 @@ export class Assistant extends Schema.Class<Assistant>("Session.Message.Assistan
       write: Schema.Finite,
     }),
   }).pipe(Schema.optional),
-  error: SessionEvent.Step.Failed.fields.data.fields.error.pipe(Schema.optional),
+  error: SessionEvent.Step.Failed.data.fields.error.pipe(Schema.optional),
   time: Schema.Struct({
     created: V2Schema.DateTimeUtcFromMillis,
     completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
@@ -156,7 +156,7 @@ export class Assistant extends Schema.Class<Assistant>("Session.Message.Assistan
 
 export class Compaction extends Schema.Class<Compaction>("Session.Message.Compaction")({
   type: Schema.Literal("compaction"),
-  reason: SessionEvent.Compaction.Started.fields.data.fields.reason,
+  reason: SessionEvent.Compaction.Started.data.fields.reason,
   summary: Schema.String,
   include: Schema.String.pipe(Schema.optional),
   ...Base,

+ 13 - 0
packages/core/src/session.ts

@@ -0,0 +1,13 @@
+export * as Session from "./session"
+
+import { Schema } from "effect"
+import { withStatics } from "./schema"
+import { Identifier } from "./util/identifier"
+
+export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
+  Schema.brand("SessionID"),
+  withStatics((schema) => ({
+    descending: (id?: string) => schema.make(id ?? "ses_" + Identifier.descending()),
+  })),
+)
+export type ID = typeof ID.Type

+ 36 - 4
packages/core/test/catalog.test.ts

@@ -1,14 +1,21 @@
 import { describe, expect } from "bun:test"
-import { DateTime, Effect, Layer, Option } from "effect"
+import { DateTime, Effect, Fiber, Layer, Option, Stream } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
-import { Instance } from "@opencode-ai/core/instance"
+import { EventV2 } from "@opencode-ai/core/event"
+import { Location } from "@opencode-ai/core/location"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { testEffect } from "./lib/effect"
 
-const instanceLayer = Layer.succeed(Instance.Service, Instance.Service.of({ directory: "test" }))
-const it = testEffect(Catalog.layer.pipe(Layer.provideMerge(PluginV2.defaultLayer), Layer.provide(instanceLayer)))
+const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))
+const it = testEffect(
+  Catalog.layer.pipe(
+    Layer.provideMerge(EventV2.defaultLayer),
+    Layer.provideMerge(PluginV2.defaultLayer),
+    Layer.provideMerge(locationLayer),
+  ),
+)
 
 describe("CatalogV2", () => {
   it.effect("normalizes provider baseURL into endpoint url", () =>
@@ -69,6 +76,31 @@ describe("CatalogV2", () => {
     }),
   )
 
+  it.effect("publishes model updated events", () =>
+    Effect.gen(function* () {
+      const catalog = yield* Catalog.Service
+      const events = yield* EventV2.Service
+      const providerID = ProviderV2.ID.make("test")
+      const modelID = ModelV2.ID.make("model")
+      const fiber = yield* events
+        .subscribe(Catalog.Event.ModelUpdated)
+        .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
+
+      yield* Effect.yieldNow
+      yield* catalog.provider.update(providerID, () => {})
+      yield* catalog.model.update(providerID, modelID, (model) => {
+        model.name = "Updated Model"
+      })
+      const event = Array.from(yield* Fiber.join(fiber))[0]
+
+      expect(event?.type).toBe("catalog.model.updated")
+      expect(event?.data.model.providerID).toBe(providerID)
+      expect(event?.data.model.id).toBe(modelID)
+      expect(event?.data.model.name).toBe("Updated Model")
+      expect(event?.location).toEqual({ directory: "test" })
+    }),
+  )
+
   it.effect("resolves unknown model endpoint from provider endpoint", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service

+ 133 - 0
packages/core/test/event.test.ts

@@ -0,0 +1,133 @@
+import { describe, expect } from "bun:test"
+import { Effect, Fiber, Layer, Schema, Stream } from "effect"
+import { EventV2 } from "@opencode-ai/core/event"
+import { Location } from "@opencode-ai/core/location"
+import { testEffect } from "./lib/effect"
+
+const locationLayer = Layer.succeed(
+  Location.Service,
+  Location.Service.of({ directory: "project", workspaceID: "workspace" }),
+)
+const it = testEffect(EventV2.layer.pipe(Layer.provideMerge(locationLayer)))
+const itWithoutLocation = testEffect(EventV2.layer)
+
+const Message = EventV2.define({
+  type: "test.message",
+  schema: {
+    text: Schema.String,
+  },
+})
+
+const GlobalMessage = EventV2.define({
+  type: "test.global",
+  schema: {
+    text: Schema.String,
+  },
+})
+
+const VersionedMessage = EventV2.define({
+  type: "test.versioned",
+  version: 2,
+  schema: {
+    text: Schema.String,
+  },
+})
+
+describe("EventV2", () => {
+  it.effect("publishes events with the current location", () =>
+    Effect.gen(function* () {
+      const events = yield* EventV2.Service
+      const fiber = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
+      yield* Effect.yieldNow
+      const event = yield* events.publish(Message, { text: "hello" })
+      const received = Array.from(yield* Fiber.join(fiber))
+
+      expect(received).toEqual([event])
+      expect(event.type).toBe("test.message")
+      expect(event).not.toHaveProperty("version")
+      expect(event.data).toEqual({ text: "hello" })
+      expect(event.location).toEqual({ directory: "project", workspaceID: "workspace" })
+    }),
+  )
+
+  itWithoutLocation.effect("omits location when no location is available", () =>
+    Effect.gen(function* () {
+      const events = yield* EventV2.Service
+      const event = yield* events.publish(GlobalMessage, { text: "hello" })
+
+      expect(event).not.toHaveProperty("location")
+      expect(event.type).toBe("test.global")
+    }),
+  )
+
+  it.effect("publishes definition version", () =>
+    Effect.gen(function* () {
+      const events = yield* EventV2.Service
+      const event = yield* events.publish(VersionedMessage, { text: "hello" })
+
+      expect(event.type).toBe("test.versioned")
+      expect(event.version).toBe(2)
+    }),
+  )
+
+  it.effect("stores definitions in the exported registry", () =>
+    Effect.sync(() => {
+      expect(EventV2.registry.get(Message.type)).toBe(Message)
+    }),
+  )
+
+  it.effect("publishes to typed and wildcard subscriptions", () =>
+    Effect.gen(function* () {
+      const events = yield* EventV2.Service
+      const typed = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
+      const wildcard = yield* events.all().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
+      yield* Effect.yieldNow
+      const event = yield* events.publish(Message, { text: "hello" })
+
+      expect(Array.from(yield* Fiber.join(typed))).toEqual([event])
+      expect(Array.from(yield* Fiber.join(wildcard))).toEqual([event])
+    }),
+  )
+
+  it.effect("runs sync handlers inline", () =>
+    Effect.gen(function* () {
+      const events = yield* EventV2.Service
+      const received = new Array<EventV2.Payload>()
+      const unsubscribe = yield* events.sync((event) =>
+        Effect.sync(() => {
+          received.push(event)
+        }),
+      )
+
+      const event = yield* events.publish(Message, { text: "hello" })
+      yield* unsubscribe
+      yield* events.publish(Message, { text: "after unsubscribe" })
+
+      expect(received).toEqual([event])
+    }),
+  )
+
+  it.effect("runs sync handlers before publishing to streams", () =>
+    Effect.gen(function* () {
+      const events = yield* EventV2.Service
+      const received = new Array<string>()
+      const fiber = yield* events.all().pipe(
+        Stream.take(1),
+        Stream.runForEach(() => Effect.sync(() => received.push("stream"))),
+        Effect.forkScoped,
+      )
+      yield* events.sync((event) =>
+        Effect.sync(() => {
+          received.push(event.type)
+        }),
+      )
+
+      yield* Effect.yieldNow
+      yield* events.publish(Message, { text: "hello" })
+      yield* Fiber.join(fiber)
+
+      expect(received).toEqual([Message.type, "stream"])
+    }),
+  )
+
+})

+ 3 - 3
packages/core/test/plugin/provider-opencode.test.ts

@@ -1,7 +1,7 @@
 import { describe, expect } from "bun:test"
 import { DateTime, Effect, Layer, Option } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
-import { Instance } from "@opencode-ai/core/instance"
+import { Location } from "@opencode-ai/core/location"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode"
@@ -9,7 +9,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
 import { it, model, provider, withEnv } from "./provider-helper"
 
 const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
-const instanceLayer = Layer.succeed(Instance.Service, Instance.Service.of({ directory: "test" }))
+const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" }))
 
 describe("OpencodePlugin", () => {
   it.effect("uses a public key and cancels paid models without credentials", () =>
@@ -192,6 +192,6 @@ describe("OpencodePlugin", () => {
       const selected = yield* catalog.model.small(providerID)
 
       expect(Option.getOrUndefined(selected)?.id).toBe(ModelV2.ID.make("gpt-5-nano"))
-    }).pipe(Effect.provide(Catalog.defaultLayer.pipe(Layer.provide(instanceLayer)))),
+    }).pipe(Effect.provide(Catalog.defaultLayer.pipe(Layer.provide(locationLayer)))),
   )
 })

+ 23 - 10
packages/opencode/src/bus/bus-event.ts

@@ -1,4 +1,5 @@
 import { Schema } from "effect"
+import { EventV2 } from "@opencode-ai/core/event"
 
 export type Definition<Type extends string = string, Properties extends Schema.Top = Schema.Top> = {
   type: Type
@@ -17,16 +18,28 @@ export function define<Type extends string, Properties extends Schema.Top>(
 }
 
 export function effectPayloads() {
-  return registry
-    .entries()
-    .map(([type, def]) =>
-      Schema.Struct({
-        id: Schema.String,
-        type: Schema.Literal(type),
-        properties: def.properties,
-      }).annotate({ identifier: `Event.${type}` }),
-    )
-    .toArray()
+  return [
+    ...registry
+      .entries()
+      .map(([type, def]) =>
+        Schema.Struct({
+          id: Schema.String,
+          type: Schema.Literal(type),
+          properties: def.properties,
+        }).annotate({ identifier: `Event.${type}` }),
+      )
+      .toArray(),
+    ...EventV2.registry
+      .values()
+      .map((definition) =>
+        Schema.Struct({
+          id: Schema.String,
+          type: Schema.Literal(definition.type),
+          properties: definition.data,
+        }).annotate({ identifier: `Event.${definition.type}` }),
+      )
+      .toArray(),
+  ]
 }
 
 export * as BusEvent from "./bus-event"

+ 3 - 3
packages/opencode/src/cli/cmd/debug/v2.ts

@@ -1,11 +1,11 @@
 import { EOL } from "os"
 import { Effect, Layer, Option } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
-import { InstanceServiceMap } from "@opencode-ai/core/instance-layer"
+import { LocationServiceMap } from "@opencode-ai/core/location-layer"
 import { PluginBoot } from "@opencode-ai/core/plugin/boot"
 import { effectCmd } from "../../effect-cmd"
 
-const Runtime = Layer.mergeAll(InstanceServiceMap.layer)
+const Runtime = Layer.mergeAll(LocationServiceMap.layer)
 
 export const V2Command = effectCmd({
   command: "v2",
@@ -37,7 +37,7 @@ export const V2Command = effectCmd({
       process.stdout.write(JSON.stringify(result, null, 2) + EOL)
     },
     Effect.provide(
-      InstanceServiceMap.get({
+      LocationServiceMap.get({
         directory: process.cwd(),
       }),
     ),

+ 2 - 0
packages/opencode/src/effect/app-runtime.ts

@@ -56,6 +56,7 @@ import { Npm } from "@opencode-ai/core/npm"
 import { memoMap } from "@opencode-ai/core/effect/memo-map"
 import { DataMigration } from "@/data-migration"
 import { BackgroundJob } from "@/background/job"
+import { EventV2Bridge } from "@/event-v2-bridge"
 import { RuntimeFlags } from "@/effect/runtime-flags"
 
 export const AppLayer = Layer.mergeAll(
@@ -111,6 +112,7 @@ export const AppLayer = Layer.mergeAll(
   ShareNext.defaultLayer,
   SessionShare.defaultLayer,
   SyncEvent.defaultLayer,
+  EventV2Bridge.defaultLayer,
   DataMigration.defaultLayer,
 ).pipe(Layer.provideMerge(InstanceLayer.layer), Layer.provideMerge(Observability.layer))
 

+ 99 - 0
packages/opencode/src/event-v2-bridge.ts

@@ -0,0 +1,99 @@
+// Temporary V2 bridge: core events are the publish path, but the rest of
+// opencode and the HTTP event stream still expect legacy bus/sync payloads.
+// This layer goes away once consumers subscribe to core EventV2 directly.
+import { Bus as ProjectBus } from "@/bus"
+import { GlobalBus } from "@/bus/global"
+import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
+import { InstanceStore } from "@/project/instance-store"
+import { SyncEvent } from "@/sync"
+import { EventV2 } from "@opencode-ai/core/event"
+import "@opencode-ai/core/catalog"
+import "@opencode-ai/core/session-event"
+import { Context, Effect, Layer, Option } from "effect"
+
+const syncDefinitions = new WeakMap<EventV2.Definition, SyncEvent.Definition>()
+
+export function toSyncDefinition<D extends EventV2.Definition>(
+  definition: D,
+): SyncEvent.Definition<D["type"], D["data"], D["data"]> {
+  const cached = syncDefinitions.get(definition)
+  if (cached) return cached as SyncEvent.Definition<D["type"], D["data"], D["data"]>
+  if (definition.version === undefined)
+    throw new Error(`Event.toSyncDefinition: version required for ${definition.type}`)
+  if (!definition.aggregate) throw new Error(`Event.toSyncDefinition: aggregate required for ${definition.type}`)
+  const result = {
+    type: definition.type,
+    version: definition.version,
+    aggregate: definition.aggregate,
+    schema: definition.data,
+    properties: definition.data,
+  }
+  syncDefinitions.set(definition, result)
+  return result
+}
+
+export class Service extends Context.Service<Service, EventV2.Interface>()("@opencode/EventV2Bridge") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const events = yield* EventV2.Service
+    const bus = yield* ProjectBus.Service
+    const sync = yield* SyncEvent.Service
+
+    const publishGlobal = (event: EventV2.Payload) =>
+      Effect.sync(() => {
+        GlobalBus.emit("event", {
+          workspace: event.location?.workspaceID,
+          payload: {
+            id: event.id,
+            type: event.type,
+            properties: event.data,
+          },
+        })
+      })
+
+    const provideEventLocation = <E, R>(event: EventV2.Payload, effect: Effect.Effect<void, E, R>) => {
+      return Effect.gen(function* () {
+        const ctx = yield* InstanceRef
+        if (ctx) return yield* effect
+        const store = Option.getOrUndefined(yield* Effect.serviceOption(InstanceStore.Service))
+        if (!event.location?.directory || !store) return yield* publishGlobal(event)
+        return yield* store.load({ directory: event.location.directory }).pipe(
+          Effect.flatMap((ctx) => {
+            const withInstance = effect.pipe(Effect.provideService(InstanceRef, ctx))
+            if (!event.location?.workspaceID) return withInstance
+            return withInstance.pipe(Effect.provideService(WorkspaceRef, event.location.workspaceID))
+          }),
+        )
+      })
+    }
+
+    const unsubscribe = yield* events.sync((event) => {
+      const definition = EventV2.registry.get(event.type)
+      if (!definition) return Effect.void
+      const aggregateID = definition.aggregate
+        ? (event.data as Record<string, unknown>)[definition.aggregate]
+        : undefined
+
+      if (definition.version !== undefined && typeof aggregateID === "string") {
+        return provideEventLocation(event, sync.run(toSyncDefinition(definition), event.data))
+      }
+
+      return provideEventLocation(
+        event,
+        bus.publish({ type: definition.type, properties: definition.data }, event.data, { id: event.id }),
+      )
+    })
+    yield* Effect.addFinalizer(() => unsubscribe)
+    return Service.of(events)
+  }),
+)
+
+export const defaultLayer = layer.pipe(
+  Layer.provideMerge(EventV2.defaultLayer),
+  Layer.provide(SyncEvent.defaultLayer),
+  Layer.provide(ProjectBus.defaultLayer),
+)
+
+export * as EventV2Bridge from "./event-v2-bridge"

+ 1 - 0
packages/opencode/src/server/routes/instance/httpapi/groups/global.ts

@@ -1,6 +1,7 @@
 import { Config } from "@/config/config"
 import { BusEvent } from "@/bus/bus-event"
 import { SyncEvent } from "@/sync"
+import "@/event-v2-bridge"
 import "@/server/event"
 import { Schema } from "effect"
 import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"

+ 18 - 18
packages/opencode/src/server/routes/instance/httpapi/groups/v2/instance.ts → packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts

@@ -1,28 +1,28 @@
 import { Catalog } from "@opencode-ai/core/catalog"
-import { Instance } from "@opencode-ai/core/instance"
-import { InstanceServiceMap } from "@opencode-ai/core/instance-layer"
+import { Location } from "@opencode-ai/core/location"
+import { LocationServiceMap } from "@opencode-ai/core/location-layer"
 import { PluginBoot } from "@opencode-ai/core/plugin/boot"
 import { Effect, Layer, Schema } from "effect"
 import { HttpServerRequest } from "effect/unstable/http"
 import { HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi"
 
-export const InstanceQuery = Schema.Struct({
-  instance: Schema.optional(
+export const LocationQuery = Schema.Struct({
+  location: Schema.optional(
     Schema.Struct({
       directory: Schema.optional(Schema.String),
       workspace: Schema.optional(Schema.String),
     }),
   ),
-}).annotate({ identifier: "V2InstanceQuery" })
+}).annotate({ identifier: "V2LocationQuery" })
 
-export const instanceQueryOpenApi = OpenApi.annotations({
+export const locationQueryOpenApi = OpenApi.annotations({
   transform: (operation) => {
     const parameters = operation.parameters
     if (!Array.isArray(parameters)) return operation
     return {
       ...operation,
       parameters: parameters.map((parameter) =>
-        parameter?.name === "instance" && parameter?.in === "query"
+        parameter?.name === "location" && parameter?.in === "query"
           ? { ...parameter, style: "deepObject", explode: true }
           : parameter,
       ),
@@ -30,30 +30,30 @@ export const instanceQueryOpenApi = OpenApi.annotations({
   },
 })
 
-export class V2InstanceMiddleware extends HttpApiMiddleware.Service<
-  V2InstanceMiddleware,
+export class V2LocationMiddleware extends HttpApiMiddleware.Service<
+  V2LocationMiddleware,
   {
     provides: Catalog.Service | PluginBoot.Service
   }
->()("@opencode/ExperimentalHttpApiV2Instance") {}
+>()("@opencode/ExperimentalHttpApiV2Location") {}
 
-function ref(request: HttpServerRequest.HttpServerRequest): Instance.Ref {
+function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
   const query = new URL(request.url, "http://localhost").searchParams
   return {
-    directory: query.get("instance[directory]") || request.headers["x-opencode-directory"] || process.cwd(),
-    workspaceID: query.get("instance[workspace]") || request.headers["x-opencode-workspace"],
+    directory: query.get("location[directory]") || request.headers["x-opencode-directory"] || process.cwd(),
+    workspaceID: query.get("location[workspace]") || request.headers["x-opencode-workspace"],
   }
 }
 
 export const layer = Layer.effect(
-  V2InstanceMiddleware,
+  V2LocationMiddleware,
   Effect.gen(function* () {
-    const instances = yield* InstanceServiceMap
-    return V2InstanceMiddleware.of((effect) =>
+    const locations = yield* LocationServiceMap
+    return V2LocationMiddleware.of((effect) =>
       Effect.gen(function* () {
         const request = yield* HttpServerRequest.HttpServerRequest
-        return yield* effect.pipe(Effect.provide(instances.get(ref(request))))
+        return yield* effect.pipe(Effect.provide(locations.get(ref(request))))
       }),
     )
   }),
-).pipe(Layer.provide(InstanceServiceMap.layer))
+).pipe(Layer.provide(LocationServiceMap.layer))

+ 1 - 1
packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts

@@ -1,5 +1,5 @@
 import { SessionID } from "@/session/schema"
-import { SessionMessage } from "@/v2/session-message"
+import { SessionMessage } from "@opencode-ai/core/session-message"
 import { Schema } from "effect"
 import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 import { Authorization } from "../../middleware/authorization"

+ 4 - 4
packages/opencode/src/server/routes/instance/httpapi/groups/v2/model.ts

@@ -2,15 +2,15 @@ import { ModelV2 } from "@opencode-ai/core/model"
 import { Schema } from "effect"
 import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 import { Authorization } from "../../middleware/authorization"
-import { InstanceQuery, instanceQueryOpenApi, V2InstanceMiddleware } from "./instance"
+import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
 
 export const ModelGroup = HttpApiGroup.make("v2.model")
   .add(
     HttpApiEndpoint.get("models", "/api/model", {
-      query: InstanceQuery,
+      query: LocationQuery,
       success: Schema.Array(ModelV2.Info),
     })
-      .annotateMerge(instanceQueryOpenApi)
+      .annotateMerge(locationQueryOpenApi)
       .annotateMerge(
         OpenApi.annotations({
           identifier: "v2.model.list",
@@ -25,5 +25,5 @@ export const ModelGroup = HttpApiGroup.make("v2.model")
       description: "Experimental v2 model routes.",
     }),
   )
-  .middleware(V2InstanceMiddleware)
+  .middleware(V2LocationMiddleware)
   .middleware(Authorization)

+ 6 - 6
packages/opencode/src/server/routes/instance/httpapi/groups/v2/provider.ts

@@ -3,15 +3,15 @@ import { Schema } from "effect"
 import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 import { ApiNotFoundError } from "../../errors"
 import { Authorization } from "../../middleware/authorization"
-import { InstanceQuery, instanceQueryOpenApi, V2InstanceMiddleware } from "./instance"
+import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
 
 export const ProviderGroup = HttpApiGroup.make("v2.provider")
   .add(
     HttpApiEndpoint.get("providers", "/api/provider", {
-      query: InstanceQuery,
+      query: LocationQuery,
       success: Schema.Array(ProviderV2.Info),
     })
-      .annotateMerge(instanceQueryOpenApi)
+      .annotateMerge(locationQueryOpenApi)
       .annotateMerge(
         OpenApi.annotations({
           identifier: "v2.provider.list",
@@ -23,11 +23,11 @@ export const ProviderGroup = HttpApiGroup.make("v2.provider")
   .add(
     HttpApiEndpoint.get("provider", "/api/provider/:providerID", {
       params: { providerID: ProviderV2.ID },
-      query: InstanceQuery,
+      query: LocationQuery,
       success: ProviderV2.Info,
       error: ApiNotFoundError,
     })
-      .annotateMerge(instanceQueryOpenApi)
+      .annotateMerge(locationQueryOpenApi)
       .annotateMerge(
         OpenApi.annotations({
           identifier: "v2.provider.get",
@@ -43,5 +43,5 @@ export const ProviderGroup = HttpApiGroup.make("v2.provider")
       description: "Experimental v2 provider routes.",
     }),
   )
-  .middleware(V2InstanceMiddleware)
+  .middleware(V2LocationMiddleware)
   .middleware(Authorization)

+ 1 - 1
packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts

@@ -1,5 +1,5 @@
 import { SessionID } from "@/session/schema"
-import { SessionMessage } from "@/v2/session-message"
+import { SessionMessage } from "@opencode-ai/core/session-message"
 import { Prompt } from "@opencode-ai/core/session-prompt"
 import { SessionV2 } from "@/v2/session"
 import { Schema } from "effect"

+ 2 - 2
packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts

@@ -1,12 +1,12 @@
 import { SessionV2 } from "@/v2/session"
 import { Layer } from "effect"
-import { layer as v2InstanceLayer } from "../groups/v2/instance"
+import { layer as v2LocationLayer } from "../groups/v2/location"
 import { messageHandlers } from "./v2/message"
 import { modelHandlers } from "./v2/model"
 import { providerHandlers } from "./v2/provider"
 import { sessionHandlers } from "./v2/session"
 
 export const v2Handlers = Layer.mergeAll(sessionHandlers, messageHandlers, modelHandlers, providerHandlers).pipe(
-  Layer.provide(v2InstanceLayer),
+  Layer.provide(v2LocationLayer),
   Layer.provide(SessionV2.defaultLayer),
 )

+ 1 - 1
packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts

@@ -1,4 +1,4 @@
-import { SessionMessage } from "@/v2/session-message"
+import { SessionMessage } from "@opencode-ai/core/session-message"
 import { SessionV2 } from "@/v2/session"
 import { Effect, Schema } from "effect"
 import * as DateTime from "effect/DateTime"

+ 2 - 0
packages/opencode/src/server/routes/instance/httpapi/server.ts

@@ -45,6 +45,7 @@ import { SessionSummary } from "@/session/summary"
 import { Todo } from "@/session/todo"
 import { SessionShare } from "@/share/session"
 import { ShareNext } from "@/share/share-next"
+import { EventV2Bridge } from "@/event-v2-bridge"
 import { Skill } from "@/skill"
 import { Snapshot } from "@/snapshot"
 import { SyncEvent } from "@/sync"
@@ -221,6 +222,7 @@ export function createRoutes(
       ShareNext.defaultLayer,
       Snapshot.defaultLayer,
       SyncEvent.defaultLayer,
+      EventV2Bridge.defaultLayer,
       Skill.defaultLayer,
       Todo.defaultLayer,
       ToolRegistry.defaultLayer,

+ 8 - 19
packages/opencode/src/session/compaction.ts

@@ -19,8 +19,9 @@ import { isOverflow as overflow, usable } from "./overflow"
 import { makeRuntime } from "@/effect/run-service"
 import { serviceUse } from "@/effect/service-use"
 import { RuntimeFlags } from "@/effect/runtime-flags"
-import { SyncEvent } from "@/sync"
-import { SessionEvent } from "@/v2/session-event"
+import { EventV2 } from "@opencode-ai/core/event"
+import { EventV2Bridge } from "@/event-v2-bridge"
+import { SessionEvent } from "@opencode-ai/core/session-event"
 
 const log = Log.create({ service: "session.compaction" })
 
@@ -210,19 +211,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Se
 
 export const use = serviceUse(Service)
 
-export const layer: Layer.Layer<
-  Service,
-  never,
-  | Bus.Service
-  | Config.Service
-  | Session.Service
-  | Agent.Service
-  | Plugin.Service
-  | SessionProcessor.Service
-  | Provider.Service
-  | SyncEvent.Service
-  | RuntimeFlags.Service
-> = Layer.effect(
+export const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const bus = yield* Bus.Service
@@ -232,7 +221,7 @@ export const layer: Layer.Layer<
     const plugin = yield* Plugin.Service
     const processors = yield* SessionProcessor.Service
     const provider = yield* Provider.Service
-    const sync = yield* SyncEvent.Service
+    const events = yield* EventV2Bridge.Service
     const flags = yield* RuntimeFlags.Service
 
     const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: {
@@ -577,7 +566,7 @@ export const layer: Layer.Layer<
           },
         )
         if (flags.experimentalEventSystem) {
-          yield* sync.run(SessionEvent.Compaction.Ended.Sync, {
+          yield* events.publish(SessionEvent.Compaction.Ended, {
             sessionID: input.sessionID,
             timestamp: DateTime.makeUnsafe(Date.now()),
             text: summary ?? "",
@@ -613,7 +602,7 @@ export const layer: Layer.Layer<
         overflow: input.overflow,
       })
       if (flags.experimentalEventSystem) {
-        yield* sync.run(SessionEvent.Compaction.Started.Sync, {
+        yield* events.publish(SessionEvent.Compaction.Started, {
           sessionID: input.sessionID,
           timestamp: DateTime.makeUnsafe(Date.now()),
           reason: input.auto ? "auto" : "manual",
@@ -639,8 +628,8 @@ export const defaultLayer = Layer.suspend(() =>
     Layer.provide(Plugin.defaultLayer),
     Layer.provide(Bus.layer),
     Layer.provide(Config.defaultLayer),
-    Layer.provide(SyncEvent.defaultLayer),
     Layer.provide(RuntimeFlags.defaultLayer),
+    Layer.provide(EventV2Bridge.defaultLayer),
   ),
 )
 

+ 19 - 34
packages/opencode/src/session/processor.ts

@@ -21,8 +21,9 @@ import { Question } from "@/question"
 import { errorMessage } from "@/util/error"
 import * as Log from "@opencode-ai/core/util/log"
 import { isRecord } from "@/util/record"
-import { SyncEvent } from "@/sync"
-import { SessionEvent } from "@/v2/session-event"
+import { EventV2 } from "@opencode-ai/core/event"
+import { EventV2Bridge } from "@/event-v2-bridge"
+import { SessionEvent } from "@opencode-ai/core/session-event"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import * as DateTime from "effect/DateTime"
@@ -84,23 +85,7 @@ type StreamEvent = Event
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/SessionProcessor") {}
 
-export const layer: Layer.Layer<
-  Service,
-  never,
-  | Session.Service
-  | Config.Service
-  | Bus.Service
-  | Snapshot.Service
-  | Agent.Service
-  | LLM.Service
-  | Permission.Service
-  | Plugin.Service
-  | Image.Service
-  | SessionSummary.Service
-  | SessionStatus.Service
-  | SyncEvent.Service
-  | RuntimeFlags.Service
-> = Layer.effect(
+export const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const session = yield* Session.Service
@@ -115,7 +100,7 @@ export const layer: Layer.Layer<
     const scope = yield* Scope.Scope
     const status = yield* SessionStatus.Service
     const image = yield* Image.Service
-    const sync = yield* SyncEvent.Service
+    const events = yield* EventV2Bridge.Service
     const flags = yield* RuntimeFlags.Service
 
     const create = Effect.fn("SessionProcessor.create")(function* (input: Input) {
@@ -236,7 +221,7 @@ export const layer: Layer.Layer<
             if (value.id in ctx.reasoningMap) return
             // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
             if (flags.experimentalEventSystem) {
-              yield* sync.run(SessionEvent.Reasoning.Started.Sync, {
+              yield* events.publish(SessionEvent.Reasoning.Started, {
                 sessionID: ctx.sessionID,
                 reasoningID: value.id,
                 timestamp: DateTime.makeUnsafe(Date.now()),
@@ -271,7 +256,7 @@ export const layer: Layer.Layer<
             if (!(value.id in ctx.reasoningMap)) return
             // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
             if (flags.experimentalEventSystem) {
-              yield* sync.run(SessionEvent.Reasoning.Ended.Sync, {
+              yield* events.publish(SessionEvent.Reasoning.Ended, {
                 sessionID: ctx.sessionID,
                 reasoningID: value.id,
                 text: ctx.reasoningMap[value.id].text,
@@ -292,7 +277,7 @@ export const layer: Layer.Layer<
             }
             // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
             if (flags.experimentalEventSystem) {
-              yield* sync.run(SessionEvent.Tool.Input.Started.Sync, {
+              yield* events.publish(SessionEvent.Tool.Input.Started, {
                 sessionID: ctx.sessionID,
                 callID: value.id,
                 name: value.toolName,
@@ -323,7 +308,7 @@ export const layer: Layer.Layer<
           case "tool-input-end": {
             // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
             if (flags.experimentalEventSystem) {
-              yield* sync.run(SessionEvent.Tool.Input.Ended.Sync, {
+              yield* events.publish(SessionEvent.Tool.Input.Ended, {
                 sessionID: ctx.sessionID,
                 callID: value.id,
                 text: "",
@@ -340,7 +325,7 @@ export const layer: Layer.Layer<
             const toolCall = yield* readToolCall(value.toolCallId)
             // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
             if (flags.experimentalEventSystem) {
-              yield* sync.run(SessionEvent.Tool.Called.Sync, {
+              yield* events.publish(SessionEvent.Tool.Called, {
                 sessionID: ctx.sessionID,
                 callID: value.toolCallId,
                 tool: value.toolName,
@@ -428,7 +413,7 @@ export const layer: Layer.Layer<
             }
             // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
             if (flags.experimentalEventSystem) {
-              yield* sync.run(SessionEvent.Tool.Success.Sync, {
+              yield* events.publish(SessionEvent.Tool.Success, {
                 sessionID: ctx.sessionID,
                 callID: value.toolCallId,
                 structured: output.metadata,
@@ -458,7 +443,7 @@ export const layer: Layer.Layer<
             const toolCall = yield* readToolCall(value.toolCallId)
             // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
             if (flags.experimentalEventSystem) {
-              yield* sync.run(SessionEvent.Tool.Failed.Sync, {
+              yield* events.publish(SessionEvent.Tool.Failed, {
                 sessionID: ctx.sessionID,
                 callID: value.toolCallId,
                 error: {
@@ -483,7 +468,7 @@ export const layer: Layer.Layer<
             if (!ctx.assistantMessage.summary) {
               // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
               if (flags.experimentalEventSystem) {
-                yield* sync.run(SessionEvent.Step.Started.Sync, {
+                yield* events.publish(SessionEvent.Step.Started, {
                   sessionID: ctx.sessionID,
                   agent: input.assistantMessage.agent,
                   model: {
@@ -515,7 +500,7 @@ export const layer: Layer.Layer<
             if (!ctx.assistantMessage.summary) {
               // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
               if (flags.experimentalEventSystem) {
-                yield* sync.run(SessionEvent.Step.Ended.Sync, {
+                yield* events.publish(SessionEvent.Step.Ended, {
                   sessionID: ctx.sessionID,
                   finish: value.finishReason,
                   cost: usage.cost,
@@ -572,7 +557,7 @@ export const layer: Layer.Layer<
             if (!ctx.assistantMessage.summary) {
               // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
               if (flags.experimentalEventSystem) {
-                yield* sync.run(SessionEvent.Text.Started.Sync, {
+                yield* events.publish(SessionEvent.Text.Started, {
                   sessionID: ctx.sessionID,
                   timestamp: DateTime.makeUnsafe(Date.now()),
                 })
@@ -619,7 +604,7 @@ export const layer: Layer.Layer<
             if (!ctx.assistantMessage.summary) {
               // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
               if (flags.experimentalEventSystem) {
-                yield* sync.run(SessionEvent.Text.Ended.Sync, {
+                yield* events.publish(SessionEvent.Text.Ended, {
                   sessionID: ctx.sessionID,
                   text: ctx.currentText.text,
                   timestamp: DateTime.makeUnsafe(Date.now()),
@@ -715,7 +700,7 @@ export const layer: Layer.Layer<
         if (!ctx.assistantMessage.summary) {
           // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
           if (flags.experimentalEventSystem) {
-            yield* sync.run(SessionEvent.Step.Failed.Sync, {
+            yield* events.publish(SessionEvent.Step.Failed, {
               sessionID: ctx.sessionID,
               error: {
                 type: "unknown",
@@ -769,7 +754,7 @@ export const layer: Layer.Layer<
                 set: (info) => {
                   // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
                   const event = flags.experimentalEventSystem
-                    ? sync.run(SessionEvent.Retried.Sync, {
+                    ? events.publish(SessionEvent.Retried, {
                         sessionID: ctx.sessionID,
                         attempt: info.attempt,
                         error: {
@@ -830,8 +815,8 @@ export const defaultLayer = Layer.suspend(() =>
     Layer.provide(Image.defaultLayer),
     Layer.provide(Bus.layer),
     Layer.provide(Config.defaultLayer),
-    Layer.provide(SyncEvent.defaultLayer),
     Layer.provide(RuntimeFlags.defaultLayer),
+    Layer.provide(EventV2Bridge.defaultLayer),
   ),
 )
 

+ 29 - 28
packages/opencode/src/session/projectors-next.ts

@@ -1,10 +1,11 @@
 import { and, desc, eq } from "@/storage/db"
 import type { Database } from "@/storage/db"
-import { SessionMessage } from "@/v2/session-message"
-import { SessionMessageUpdater } from "@/v2/session-message-updater"
-import { SessionEvent } from "@/v2/session-event"
+import { SessionMessage } from "@opencode-ai/core/session-message"
+import { SessionMessageUpdater } from "@opencode-ai/core/session-message-updater"
+import { SessionEvent } from "@opencode-ai/core/session-event"
 import * as DateTime from "effect/DateTime"
 import { SyncEvent } from "@/sync"
+import { EventV2Bridge } from "@/event-v2-bridge"
 import { SessionMessageTable, SessionTable } from "./session.sql"
 import type { SessionID } from "./schema"
 import { Schema } from "effect"
@@ -119,7 +120,7 @@ function update(db: Database.TxOrDb, event: SessionEvent.Event) {
 }
 
 export default [
-  SyncEvent.project(SessionEvent.AgentSwitched.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.AgentSwitched), (db, data, event) => {
     db.update(SessionTable)
       .set({
         agent: data.agent,
@@ -129,7 +130,7 @@ export default [
       .run()
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.agent.switched", data })
   }),
-  SyncEvent.project(SessionEvent.ModelSwitched.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.ModelSwitched), (db, data, event) => {
     db.update(SessionTable)
       .set({
         model: data.model,
@@ -139,65 +140,65 @@ export default [
       .run()
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.model.switched", data })
   }),
-  SyncEvent.project(SessionEvent.Prompted.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Prompted), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.prompted", data })
   }),
-  SyncEvent.project(SessionEvent.Synthetic.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Synthetic), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.synthetic", data })
   }),
-  SyncEvent.project(SessionEvent.Shell.Started.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Shell.Started), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.shell.started", data })
   }),
-  SyncEvent.project(SessionEvent.Shell.Ended.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Shell.Ended), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.shell.ended", data })
   }),
-  SyncEvent.project(SessionEvent.Step.Started.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Step.Started), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.step.started", data })
   }),
-  SyncEvent.project(SessionEvent.Step.Ended.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Step.Ended), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.step.ended", data })
   }),
-  SyncEvent.project(SessionEvent.Step.Failed.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Step.Failed), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.step.failed", data })
   }),
-  SyncEvent.project(SessionEvent.Text.Started.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Text.Started), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.text.started", data })
   }),
-  SyncEvent.project(SessionEvent.Text.Delta.Sync, () => {}),
-  SyncEvent.project(SessionEvent.Text.Ended.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Text.Delta), () => {}),
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Text.Ended), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.text.ended", data })
   }),
-  SyncEvent.project(SessionEvent.Tool.Input.Started.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Tool.Input.Started), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.input.started", data })
   }),
-  SyncEvent.project(SessionEvent.Tool.Input.Delta.Sync, () => {}),
-  SyncEvent.project(SessionEvent.Tool.Input.Ended.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Tool.Input.Delta), () => {}),
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Tool.Input.Ended), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.input.ended", data })
   }),
-  SyncEvent.project(SessionEvent.Tool.Called.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Tool.Called), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.called", data })
   }),
-  SyncEvent.project(SessionEvent.Tool.Success.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Tool.Success), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.success", data })
   }),
-  SyncEvent.project(SessionEvent.Tool.Failed.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Tool.Failed), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.failed", data })
   }),
-  SyncEvent.project(SessionEvent.Reasoning.Started.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Reasoning.Started), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.reasoning.started", data })
   }),
-  SyncEvent.project(SessionEvent.Reasoning.Delta.Sync, () => {}),
-  SyncEvent.project(SessionEvent.Reasoning.Ended.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Reasoning.Delta), () => {}),
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Reasoning.Ended), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.reasoning.ended", data })
   }),
-  SyncEvent.project(SessionEvent.Retried.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Retried), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.retried", data })
   }),
-  SyncEvent.project(SessionEvent.Compaction.Started.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Compaction.Started), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.started", data })
   }),
-  SyncEvent.project(SessionEvent.Compaction.Delta.Sync, () => {}),
-  SyncEvent.project(SessionEvent.Compaction.Ended.Sync, (db, data, event) => {
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Compaction.Delta), () => {}),
+  SyncEvent.project(EventV2Bridge.toSyncDefinition(SessionEvent.Compaction.Ended), (db, data, event) => {
     update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.ended", data })
   }),
 ]

+ 12 - 12
packages/opencode/src/session/prompt.ts

@@ -52,8 +52,9 @@ import { TaskTool, type TaskPromptOps } from "@/tool/task"
 import { SessionRunState } from "./run-state"
 import { EffectBridge } from "@/effect/bridge"
 import { RuntimeFlags } from "@/effect/runtime-flags"
-import { SyncEvent } from "@/sync"
-import { SessionEvent } from "@/v2/session-event"
+import { EventV2 } from "@opencode-ai/core/event"
+import { EventV2Bridge } from "@/event-v2-bridge"
+import { SessionEvent } from "@opencode-ai/core/session-event"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { AgentAttachment, FileAttachment, ReferenceAttachment, Source } from "@opencode-ai/core/session-prompt"
@@ -204,7 +205,7 @@ export const layer = Layer.effect(
     const sys = yield* SystemPrompt.Service
     const llm = yield* LLM.Service
     const references = yield* Reference.Service
-    const sync = yield* SyncEvent.Service
+    const events = yield* EventV2Bridge.Service
     const flags = yield* RuntimeFlags.Service
     const runner = Effect.fn("SessionPrompt.runner")(function* () {
       return yield* EffectBridge.make()
@@ -944,7 +945,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
               providerID: model.providerID,
             }
             yield* sessions.updateMessage(msg)
-            const callID = ulid()
             const started = Date.now()
             const part: MessageV2.ToolPart = {
               type: "tool",
@@ -961,10 +961,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the
             }
             yield* sessions.updatePart(part)
             if (flags.experimentalEventSystem) {
-              yield* sync.run(SessionEvent.Shell.Started.Sync, {
+              yield* events.publish(SessionEvent.Shell.Started, {
                 sessionID: input.sessionID,
                 timestamp: DateTime.makeUnsafe(started),
-                callID,
+                callID: part.callID,
                 command: input.command,
               })
             }
@@ -984,7 +984,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
               }
               const completed = Date.now()
               if (flags.experimentalEventSystem) {
-                yield* sync.run(SessionEvent.Shell.Ended.Sync, {
+                yield* events.publish(SessionEvent.Shell.Ended, {
                   sessionID: input.sessionID,
                   timestamp: DateTime.makeUnsafe(completed),
                   callID: part.callID,
@@ -1134,7 +1134,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       }
 
       if (current?.agent !== info.agent) {
-        yield* sync.run(SessionEvent.AgentSwitched.Sync, {
+        yield* events.publish(SessionEvent.AgentSwitched, {
           sessionID: input.sessionID,
           timestamp: DateTime.makeUnsafe(info.time.created),
           agent: info.agent,
@@ -1145,7 +1145,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
         current.model.id !== info.model.modelID ||
         (current.model.variant === "default" ? undefined : current.model.variant) !== info.model.variant
       ) {
-        yield* sync.run(SessionEvent.ModelSwitched.Sync, {
+        yield* events.publish(SessionEvent.ModelSwitched, {
           sessionID: input.sessionID,
           timestamp: DateTime.makeUnsafe(info.time.created),
           model: {
@@ -1586,7 +1586,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       )
       // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
       if (flags.experimentalEventSystem) {
-        yield* sync.run(SessionEvent.Prompted.Sync, {
+        yield* events.publish(SessionEvent.Prompted, {
           sessionID: input.sessionID,
           timestamp: DateTime.makeUnsafe(info.time.created),
           prompt: {
@@ -1600,7 +1600,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       for (const text of nextPrompt.synthetic) {
         // TODO(v2): Temporary dual-write while migrating session messages to v2 events.
         if (flags.experimentalEventSystem) {
-          yield* sync.run(SessionEvent.Synthetic.Sync, {
+          yield* events.publish(SessionEvent.Synthetic, {
             sessionID: input.sessionID,
             timestamp: DateTime.makeUnsafe(info.time.created),
             text,
@@ -2038,13 +2038,13 @@ export const defaultLayer = Layer.suspend(() =>
     Layer.provide(Image.defaultLayer),
     Layer.provide(
       Layer.mergeAll(
+        EventV2Bridge.defaultLayer,
         Agent.defaultLayer,
         SystemPrompt.defaultLayer,
         LLM.defaultLayer,
         Reference.defaultLayer,
         Bus.layer,
         CrossSpawnSpawner.defaultLayer,
-        SyncEvent.defaultLayer,
         RuntimeFlags.defaultLayer,
       ),
     ),

+ 2 - 7
packages/opencode/src/session/schema.ts

@@ -1,15 +1,10 @@
 import { Schema } from "effect"
 
 import { Identifier } from "@/id/id"
+import { Session as CoreSession } from "@opencode-ai/core/session"
 import { withStatics } from "@opencode-ai/core/schema"
 
-export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
-  Schema.brand("SessionID"),
-  withStatics((s) => ({
-    descending: (id?: string) => s.make(Identifier.descending("session", id)),
-  })),
-)
-
+export const SessionID = CoreSession.ID
 export type SessionID = Schema.Schema.Type<typeof SessionID>
 
 export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(

+ 1 - 1
packages/opencode/src/session/session.sql.ts

@@ -1,7 +1,7 @@
 import { sqliteTable, text, integer, index, primaryKey, real } from "drizzle-orm/sqlite-core"
 import { ProjectTable } from "../project/project.sql"
 import type { MessageV2 } from "./message-v2"
-import type { SessionMessage } from "../v2/session-message"
+import type { SessionMessage } from "@opencode-ai/core/session-message"
 import type { Snapshot } from "../snapshot"
 import type { Permission } from "../permission"
 import type { ProjectID } from "../project/schema"

+ 47 - 16
packages/opencode/src/sync/index.ts

@@ -1,3 +1,7 @@
+// Legacy sync event system. It should stay unaware of core EventV2 execution;
+// the only temporary V2 coupling here is exposing versioned core event schemas
+// in effectPayloads() so existing HTTP/SDK schema generation remains stable.
+// Remove that registry read when event schemas are generated from core directly.
 import { Database } from "@/storage/db"
 import { eq } from "drizzle-orm"
 import { GlobalBus } from "@/bus/global"
@@ -9,6 +13,7 @@ import type { WorkspaceID } from "@/control-plane/schema"
 import { EventID } from "./schema"
 import { Context, Effect, Layer, Schema as EffectSchema } from "effect"
 import type { DeepMutable } from "@opencode-ai/core/schema"
+import { EventV2 } from "@opencode-ai/core/event"
 import { serviceUse } from "@/effect/service-use"
 import { InstanceState } from "@/effect/instance-state"
 import { RuntimeFlags } from "@/effect/runtime-flags"
@@ -221,6 +226,9 @@ export function reset() {
 }
 
 export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: ConvertEvent }) {
+  for (const [def] of input.projectors) {
+    register(def)
+  }
   projectors = new Map(input.projectors)
 
   // Install all the latest event defs to the bus. We only ever emit
@@ -269,9 +277,7 @@ export function define<
     properties: (input.busSchema ?? input.schema) as BusSchema,
   }
 
-  versions.set(def.type, Math.max(def.version, versions.get(def.type) || 0))
-
-  registry.set(versionedType(def.type, def.version), def)
+  register(def)
 
   return def
 }
@@ -280,9 +286,15 @@ export function project<Def extends Definition>(
   def: Def,
   func: (db: Database.TxOrDb, data: Event<Def>["data"], event: Event<Def>) => void,
 ): [Definition, ProjectorFunc] {
+  register(def)
   return [def, func as ProjectorFunc]
 }
 
+function register(def: Definition) {
+  versions.set(def.type, Math.max(def.version, versions.get(def.type) || 0))
+  registry.set(versionedType(def.type, def.version), def)
+}
+
 function process<Def extends Definition>(
   def: Def,
   event: Event<Def>,
@@ -355,19 +367,38 @@ function process<Def extends Definition>(
 }
 
 export function effectPayloads() {
-  return registry
-    .entries()
-    .map(([type, def]) =>
-      EffectSchema.Struct({
-        type: EffectSchema.Literal("sync"),
-        name: EffectSchema.Literal(type),
-        id: EffectSchema.String,
-        seq: EffectSchema.Finite,
-        aggregateID: EffectSchema.Literal(def.aggregate),
-        data: def.schema,
-      }).annotate({ identifier: `SyncEvent.${type}` }),
-    )
-    .toArray()
+  return [
+    ...registry
+      .entries()
+      .map(([type, def]) =>
+        EffectSchema.Struct({
+          type: EffectSchema.Literal("sync"),
+          name: EffectSchema.Literal(type),
+          id: EffectSchema.String,
+          seq: EffectSchema.Finite,
+          aggregateID: EffectSchema.Literal(def.aggregate),
+          data: def.schema,
+        }).annotate({ identifier: `SyncEvent.${type}` }),
+      )
+      .toArray(),
+    ...EventV2.registry
+      .values()
+      .filter(
+        (definition) =>
+          definition.version !== undefined && !registry.has(versionedType(definition.type, definition.version)),
+      )
+      .map((definition) =>
+        EffectSchema.Struct({
+          type: EffectSchema.Literal("sync"),
+          name: EffectSchema.Literal(versionedType(definition.type, definition.version!)),
+          id: EffectSchema.String,
+          seq: EffectSchema.Finite,
+          aggregateID: EffectSchema.String,
+          data: definition.data,
+        }).annotate({ identifier: `SyncEvent.${definition.type}` }),
+      )
+      .toArray(),
+  ]
 }
 
 export * as SyncEvent from "."

+ 0 - 43
packages/opencode/src/v2/event.ts

@@ -1,43 +0,0 @@
-import { Identifier } from "@/id/id"
-import { SyncEvent } from "@/sync"
-import { withStatics } from "@opencode-ai/core/schema"
-import * as Schema from "effect/Schema"
-
-export const ID = Schema.String.pipe(
-  Schema.brand("Event.ID"),
-  withStatics((s) => ({
-    create: () => s.make(Identifier.create("evt", "ascending")),
-  })),
-)
-export type ID = Schema.Schema.Type<typeof ID>
-
-export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
-  type: Type
-  schema: Fields
-  aggregate: string
-  version?: number
-}) {
-  const Payload = Schema.Struct({
-    id: ID,
-    metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
-    type: Schema.Literal(input.type),
-    data: Schema.Struct(input.schema),
-  }).annotate({
-    identifier: input.type,
-  })
-
-  const Sync = SyncEvent.define({
-    type: input.type,
-    version: input.version ?? 1,
-    aggregate: input.aggregate,
-    schema: Payload.fields.data,
-  })
-
-  return Object.assign(Payload, {
-    Sync,
-    version: input.version,
-    aggregate: input.aggregate,
-  })
-}
-
-export * as EventV2 from "./event"

+ 8 - 8
packages/opencode/src/v2/session.ts

@@ -4,14 +4,14 @@ import { WorkspaceID } from "@/control-plane/schema"
 import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db"
 import * as Database from "@/storage/db"
 import { Context, DateTime, Effect, Layer, Option, Schema } from "effect"
-import { SessionMessage } from "./session-message"
+import { SessionMessage } from "@opencode-ai/core/session-message"
 import type { Prompt } from "@opencode-ai/core/session-prompt"
-import { EventV2 } from "./event"
 import { ProjectID } from "@/project/schema"
-import { SessionEvent } from "./session-event"
+import { SessionEvent } from "@opencode-ai/core/session-event"
 import { V2Schema } from "@opencode-ai/core/v2-schema"
 import { optionalOmitUndefined } from "@opencode-ai/core/schema"
-import { SyncEvent } from "@/sync"
+import { EventV2 } from "@opencode-ai/core/event"
+import { EventV2Bridge } from "@/event-v2-bridge"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 
@@ -125,7 +125,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
 export const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
-    const sync = yield* SyncEvent.Service
+    const events = yield* EventV2Bridge.Service
     const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
 
     const decode = (row: typeof SessionMessageTable.$inferSelect) =>
@@ -292,14 +292,14 @@ export const layer = Layer.effect(
       shell: Effect.fn("V2Session.shell")(function* (_input) {}),
       skill: Effect.fn("V2Session.skill")(function* (_input) {}),
       switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
-        yield* sync.run(SessionEvent.AgentSwitched.Sync, {
+        yield* events.publish(SessionEvent.AgentSwitched, {
           sessionID: input.sessionID,
           timestamp: DateTime.makeUnsafe(Date.now()),
           agent: input.agent,
         })
       }),
       switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
-        yield* sync.run(SessionEvent.ModelSwitched.Sync, {
+        yield* events.publish(SessionEvent.ModelSwitched, {
           sessionID: input.sessionID,
           timestamp: DateTime.makeUnsafe(Date.now()),
           model: input.model,
@@ -334,6 +334,6 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(SyncEvent.defaultLayer))
+export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))
 
 export * as SessionV2 from "./session"

+ 1 - 1
packages/opencode/test/server/httpapi-session.test.ts

@@ -19,7 +19,7 @@ import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from ".
 import { MessageV2 } from "../../src/session/message-v2"
 import { Database } from "@/storage/db"
 import { SessionMessageTable, SessionTable } from "@/session/session.sql"
-import { SessionMessage } from "../../src/v2/session-message"
+import { SessionMessage } from "@opencode-ai/core/session-message"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import * as DateTime from "effect/DateTime"

+ 3 - 0
packages/opencode/test/session/compaction.test.ts

@@ -29,6 +29,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { TestConfig } from "../fixture/config"
 import { SyncEvent } from "@/sync"
 import { RuntimeFlags } from "@/effect/runtime-flags"
+import { EventV2Bridge } from "@/event-v2-bridge"
 
 void Log.init({ print: false })
 
@@ -227,6 +228,7 @@ const deps = Layer.mergeAll(
   Config.defaultLayer,
   SyncEvent.defaultLayer,
   RuntimeFlags.layer({ experimentalEventSystem: true }),
+  EventV2Bridge.defaultLayer,
 )
 
 const env = Layer.mergeAll(
@@ -276,6 +278,7 @@ function compactionProcessLayer(options?: CompactionProcessOptions) {
     Layer.provide(options?.config ?? Config.defaultLayer),
     Layer.provide(SyncEvent.defaultLayer),
     Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
+    Layer.provide(EventV2Bridge.defaultLayer),
   )
 }
 

+ 2 - 0
packages/opencode/test/session/processor-effect.test.ts

@@ -26,6 +26,7 @@ import { testEffect } from "../lib/effect"
 import { raw, reply, TestLLMServer } from "../lib/llm-server"
 import { SyncEvent } from "@/sync"
 import { RuntimeFlags } from "@/effect/runtime-flags"
+import { EventV2Bridge } from "@/event-v2-bridge"
 
 void Log.init({ print: false })
 
@@ -180,6 +181,7 @@ const deps = Layer.mergeAll(
   Provider.defaultLayer,
   status,
   SyncEvent.defaultLayer,
+  EventV2Bridge.defaultLayer,
 ).pipe(Layer.provideMerge(infra))
 const env = Layer.mergeAll(
   TestLLMServer.layer,

+ 2 - 0
packages/opencode/test/session/prompt.test.ts

@@ -53,6 +53,7 @@ import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
 import { reply, TestLLMServer } from "../lib/llm-server"
 import { SyncEvent } from "@/sync"
 import { RuntimeFlags } from "@/effect/runtime-flags"
+import { EventV2Bridge } from "@/event-v2-bridge"
 
 void Log.init({ print: false })
 
@@ -180,6 +181,7 @@ function makeHttp(input?: { processor?: "blocking" }) {
     BackgroundJob.defaultLayer,
     status,
     SyncEvent.defaultLayer,
+    EventV2Bridge.defaultLayer,
   ).pipe(Layer.provideMerge(infra))
   const question = Question.layer.pipe(Layer.provideMerge(deps))
   const todo = Todo.layer.pipe(Layer.provideMerge(deps))

+ 2 - 0
packages/opencode/test/session/snapshot-tool-race.test.ts

@@ -61,6 +61,7 @@ import { Format } from "../../src/format"
 import { Reference } from "../../src/reference/reference"
 import { SyncEvent } from "@/sync"
 import { RuntimeFlags } from "@/effect/runtime-flags"
+import { EventV2Bridge } from "@/event-v2-bridge"
 
 void Log.init({ print: false })
 
@@ -129,6 +130,7 @@ function makeHttp() {
     BackgroundJob.defaultLayer,
     status,
     SyncEvent.defaultLayer,
+    EventV2Bridge.defaultLayer,
   ).pipe(Layer.provideMerge(infra))
   const question = Question.layer.pipe(Layer.provideMerge(deps))
   const todo = Todo.layer.pipe(Layer.provideMerge(deps))

+ 3 - 3
packages/opencode/test/v2/session-message-updater.test.ts

@@ -1,11 +1,11 @@
 import { expect, test } from "bun:test"
 import * as DateTime from "effect/DateTime"
 import { SessionID } from "../../src/session/schema"
-import { EventV2 } from "../../src/v2/event"
+import { EventV2 } from "@opencode-ai/core/event"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { SessionEvent } from "../../src/v2/session-event"
-import { SessionMessageUpdater } from "../../src/v2/session-message-updater"
+import { SessionEvent } from "@opencode-ai/core/session-event"
+import { SessionMessageUpdater } from "@opencode-ai/core/session-message-updater"
 
 test("step snapshots carry over to assistant messages", () => {
   const state: SessionMessageUpdater.MemoryState = { messages: [] }

+ 6 - 6
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -4387,14 +4387,14 @@ export class Model extends HeyApiClient {
    */
   public list<ThrowOnError extends boolean = false>(
     parameters?: {
-      instance?: {
+      location?: {
         directory?: string
         workspace?: string
       }
     },
     options?: Options<never, ThrowOnError>,
   ) {
-    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "instance" }] }])
+    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
     return (options?.client ?? this.client).get<V2ModelListResponses, unknown, ThrowOnError>({
       url: "/api/model",
       ...options,
@@ -4411,14 +4411,14 @@ export class Provider2 extends HeyApiClient {
    */
   public list<ThrowOnError extends boolean = false>(
     parameters?: {
-      instance?: {
+      location?: {
         directory?: string
         workspace?: string
       }
     },
     options?: Options<never, ThrowOnError>,
   ) {
-    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "instance" }] }])
+    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
     return (options?.client ?? this.client).get<V2ProviderListResponses, unknown, ThrowOnError>({
       url: "/api/provider",
       ...options,
@@ -4434,7 +4434,7 @@ export class Provider2 extends HeyApiClient {
   public get<ThrowOnError extends boolean = false>(
     parameters: {
       providerID: string
-      instance?: {
+      location?: {
         directory?: string
         workspace?: string
       }
@@ -4447,7 +4447,7 @@ export class Provider2 extends HeyApiClient {
         {
           args: [
             { in: "path", key: "providerID" },
-            { in: "query", key: "instance" },
+            { in: "query", key: "location" },
           ],
         },
       ],

File diff ditekan karena terlalu besar
+ 734 - 175
packages/sdk/js/src/v2/gen/types.gen.ts


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini