Explorar o código

refactor(core): move v1 schemas into core (#30473)

Dax hai 3 meses
pai
achega
83452558f7
Modificáronse 100 ficheiros con 1260 adicións e 916 borrados
  1. 12 0
      packages/cli/src/api.ts
  2. 40 0
      packages/cli/src/cli-api.ts
  3. 66 0
      packages/cli/src/cli-builder.ts
  4. 0 31
      packages/cli/src/debug/agents.ts
  5. 0 7
      packages/cli/src/debug/index.ts
  6. 17 0
      packages/cli/src/handlers/debug/agents.ts
  7. 5 0
      packages/cli/src/handlers/migrate.ts
  8. 13 11
      packages/cli/src/index.ts
  9. 16 3
      packages/core/src/config.ts
  10. 13 13
      packages/core/src/session/projector.ts
  11. 7 7
      packages/core/src/session/sql.ts
  12. 89 0
      packages/core/src/v1/config/agent.ts
  13. 2 2
      packages/core/src/v1/config/attachment.ts
  14. 12 0
      packages/core/src/v1/config/command.ts
  15. 69 0
      packages/core/src/v1/config/config.ts
  16. 3 1
      packages/core/src/v1/config/console-state.ts
  17. 13 2
      packages/core/src/v1/config/error.ts
  18. 1 1
      packages/core/src/v1/config/formatter.ts
  19. 2 2
      packages/core/src/v1/config/layout.ts
  20. 46 9
      packages/core/src/v1/config/lsp.ts
  21. 3 3
      packages/core/src/v1/config/mcp.ts
  22. 212 0
      packages/core/src/v1/config/migrate.ts
  23. 2 8
      packages/core/src/v1/config/permission.ts
  24. 9 0
      packages/core/src/v1/config/plugin.ts
  25. 6 7
      packages/core/src/v1/config/provider.ts
  26. 24 0
      packages/core/src/v1/config/reference.ts
  27. 3 3
      packages/core/src/v1/config/server.ts
  28. 2 3
      packages/core/src/v1/config/skills.ts
  29. 1 1
      packages/core/src/v1/permission.ts
  30. 4 4
      packages/core/src/v1/session.ts
  31. 103 0
      packages/core/test/config/config.test.ts
  32. 2 1
      packages/opencode/script/schema.ts
  33. 3 3
      packages/opencode/src/acp/content.ts
  34. 2 2
      packages/opencode/src/agent/agent.ts
  35. 3 3
      packages/opencode/src/agent/subagent-permissions.ts
  36. 5 5
      packages/opencode/src/cli/cmd/debug/agent.handler.ts
  37. 5 5
      packages/opencode/src/cli/cmd/export.ts
  38. 2 2
      packages/opencode/src/cli/cmd/github.shared.ts
  39. 5 5
      packages/opencode/src/cli/cmd/import.ts
  40. 9 8
      packages/opencode/src/cli/cmd/mcp.ts
  41. 2 2
      packages/opencode/src/cli/cmd/run.ts
  42. 2 2
      packages/opencode/src/cli/cmd/tui/config/tui-schema.ts
  43. 1 1
      packages/opencode/src/cli/cmd/tui/context/sync.tsx
  44. 3 2
      packages/opencode/src/cli/cmd/tui/plugin/runtime.ts
  45. 2 1
      packages/opencode/src/cli/network.ts
  46. 6 98
      packages/opencode/src/config/agent.ts
  47. 4 14
      packages/opencode/src/config/command.ts
  48. 13 221
      packages/opencode/src/config/config.ts
  49. 1 7
      packages/opencode/src/config/markdown.ts
  50. 0 5
      packages/opencode/src/config/model-id.ts
  51. 1 1
      packages/opencode/src/config/parse.ts
  52. 6 14
      packages/opencode/src/config/plugin.ts
  53. 3 24
      packages/opencode/src/config/reference.ts
  54. 1 1
      packages/opencode/src/config/variable.ts
  55. 3 3
      packages/opencode/src/image/image.ts
  56. 13 12
      packages/opencode/src/mcp/index.ts
  57. 31 31
      packages/opencode/src/permission/index.ts
  58. 3 2
      packages/opencode/src/plugin/loader.ts
  59. 2 1
      packages/opencode/src/provider/provider.ts
  60. 4 3
      packages/opencode/src/server/routes/instance/httpapi/groups/config.ts
  61. 4 3
      packages/opencode/src/server/routes/instance/httpapi/groups/global.ts
  62. 2 2
      packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts
  63. 4 4
      packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts
  64. 12 12
      packages/opencode/src/server/routes/instance/httpapi/groups/session.ts
  65. 3 3
      packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts
  66. 5 5
      packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts
  67. 22 21
      packages/opencode/src/session/compaction.ts
  68. 5 5
      packages/opencode/src/session/instruction.ts
  69. 5 5
      packages/opencode/src/session/llm.ts
  70. 4 4
      packages/opencode/src/session/llm/request.ts
  71. 6 6
      packages/opencode/src/session/message-v2.ts
  72. 5 4
      packages/opencode/src/session/overflow.ts
  73. 21 21
      packages/opencode/src/session/processor.ts
  74. 47 47
      packages/opencode/src/session/prompt.ts
  75. 2 2
      packages/opencode/src/session/prompt/reference.ts
  76. 2 2
      packages/opencode/src/session/reminders.ts
  77. 5 5
      packages/opencode/src/session/retry.ts
  78. 4 4
      packages/opencode/src/session/revert.ts
  79. 14 14
      packages/opencode/src/session/run-state.ts
  80. 35 35
      packages/opencode/src/session/session.ts
  81. 3 3
      packages/opencode/src/session/summary.ts
  82. 3 3
      packages/opencode/src/session/tools.ts
  83. 2 1
      packages/opencode/src/skill/index.ts
  84. 3 3
      packages/opencode/src/tool/plan.ts
  85. 2 2
      packages/opencode/src/tool/task.ts
  86. 5 5
      packages/opencode/src/tool/tool.ts
  87. 2 2
      packages/opencode/test/agent/agent.test.ts
  88. 4 4
      packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts
  89. 2 2
      packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts
  90. 6 6
      packages/opencode/test/cli/github-action.test.ts
  91. 13 11
      packages/opencode/test/config/config.test.ts
  92. 3 3
      packages/opencode/test/config/lsp.test.ts
  93. 1 1
      packages/opencode/test/fixture/config.ts
  94. 6 5
      packages/opencode/test/fixture/fixture.ts
  95. 2 1
      packages/opencode/test/lib/effect.ts
  96. 3 3
      packages/opencode/test/permission-task.test.ts
  97. 51 51
      packages/opencode/test/permission/next.test.ts
  98. 2 2
      packages/opencode/test/provider/model-status.test.ts
  99. 2 2
      packages/opencode/test/server/httpapi-error-middleware.test.ts
  100. 6 5
      packages/opencode/test/server/httpapi-exercise/runner.ts

+ 12 - 0
packages/cli/src/api.ts

@@ -0,0 +1,12 @@
+import { CliApi } from "./cli-api"
+
+export const Api = CliApi.make("opencode", {
+  description: "OpenCode command line interface",
+  commands: [
+    CliApi.make("debug", {
+      description: "Debugging and troubleshooting tools",
+      commands: [CliApi.make("agents", { description: "List all agents" })],
+    }),
+    CliApi.make("migrate", { description: "Migrate v1 data to v2" }),
+  ],
+})

+ 40 - 0
packages/cli/src/cli-api.ts

@@ -0,0 +1,40 @@
+import * as Command from "effect/unstable/cli/Command"
+
+type Options<Config extends Command.Command.Config, Commands extends ReadonlyArray<Any>> = {
+  readonly description?: string
+  readonly params?: Config
+  readonly commands?: Commands
+}
+
+export interface Node<
+  Name extends string,
+  Spec extends Command.Command<Name, any, any, any, any>,
+  Commands extends Children,
+> {
+  readonly name: Name
+  readonly spec: Spec
+  readonly commands: Commands
+}
+
+export type Any = Node<string, Command.Command<any, any, any, any, any>, Children>
+export type Children = Readonly<Record<string, Any>>
+
+export function make<
+  const Name extends string,
+  const Config extends Command.Command.Config = {},
+  const Commands extends ReadonlyArray<Any> = [],
+>(name: Name, options: Options<Config, Commands> = {}) {
+  const command = Command.make(name, options.params ?? ({} as Config))
+  const spec = options.description ? command.pipe(Command.withDescription(options.description)) : command
+  return {
+    name,
+    spec,
+    commands: Object.fromEntries((options.commands ?? []).map((command) => [command.name, command])) as ChildrenOf<Commands>,
+  }
+}
+
+type ChildrenOf<Commands extends ReadonlyArray<Any>> = {
+  readonly [Node in Commands[number] as Node["name"]]: Node
+}
+
+export * as CliApi from "./cli-api"

+ 66 - 0
packages/cli/src/cli-builder.ts

@@ -0,0 +1,66 @@
+import * as Effect from "effect/Effect"
+import * as Command from "effect/unstable/cli/Command"
+import { CliApi } from "./cli-api"
+
+export type Input<Value> = Value extends CliApi.Node<infer _Name, infer Spec, infer _Commands>
+  ? Input<Spec>
+  : Value extends Command.Command<infer _Name, infer Input, infer _Context, infer _Error, infer _Requirements>
+    ? Input
+    : never
+
+type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown>
+type Loader<Node extends CliApi.Any> = () => Promise<{ default: (input: Input<Node>) => Effect.Effect<void, any> }>
+type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, never>
+
+export type Handlers<Node extends CliApi.Any> = keyof Node["commands"] extends never
+  ? Loader<Node>
+  : { readonly $?: Loader<Node> } & { readonly [Key in keyof Node["commands"]]: Handlers<Node["commands"][Key]> }
+
+interface LazyHandler {
+  readonly spec: Command.Command.Any
+  readonly load: () => Promise<{ default: RuntimeHandler }>
+}
+
+type RuntimeHandlers = (() => Promise<{ default: RuntimeHandler }>) | { readonly $?: () => Promise<{ default: RuntimeHandler }>; readonly [key: string]: RuntimeHandlers | (() => Promise<{ default: RuntimeHandler }>) | undefined }
+
+export function handler<const Node extends CliApi.Any, Error>(
+  _node: Node,
+  run: (input: Input<Node>) => Effect.Effect<void, Error>,
+) {
+  return run
+}
+
+export function handlers<const Root extends CliApi.Any>(root: Root, handlers: Handlers<Root>) {
+  const result: LazyHandler[] = []
+
+  function add(node: CliApi.Any, value: RuntimeHandlers) {
+    if (typeof value === "function") {
+      result.push({ spec: node.spec, load: value as () => Promise<{ default: RuntimeHandler }> })
+      return
+    }
+    if (value.$) result.push({ spec: node.spec, load: value.$ as () => Promise<{ default: RuntimeHandler }> })
+    for (const [name, child] of Object.entries(node.commands)) add(child, value[name] as RuntimeHandlers)
+  }
+
+  add(root, handlers as RuntimeHandlers)
+  return result
+}
+
+export function run(api: CliApi.Any, handlers: ReadonlyArray<LazyHandler>, options: { readonly version: string }) {
+  return Command.run(provide(api, handlers), options) as Effect.Effect<void, unknown, Command.Environment>
+}
+
+function provide(node: CliApi.Any, handlers: ReadonlyArray<LazyHandler>): ProvidedCommand {
+  const spec: Command.Command.Any = Object.keys(node.commands).length
+    ? (node.spec as Command.Command<string, unknown>).pipe(
+        Command.withSubcommands(Object.values(node.commands).map((child) => provide(child, handlers))),
+      )
+    : node.spec
+  const handler = handlers.find((handler) => handler.spec === node.spec)
+  if (!handler) return spec as ProvidedCommand
+  return spec.pipe(
+    Command.withHandler((input) => Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))),
+  ) as ProvidedCommand
+}
+
+export * as CliBuilder from "./cli-builder"

+ 0 - 31
packages/cli/src/debug/agents.ts

@@ -1,31 +0,0 @@
-import { EOL } from "os"
-import { AgentV2 } from "@opencode-ai/core/agent"
-import { PluginBoot } from "@opencode-ai/core/plugin/boot"
-import * as Effect from "effect/Effect"
-import * as Command from "effect/unstable/cli/Command"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
-import { AbsolutePath } from "@opencode-ai/core/schema"
-
-export const AgentsCommand = Command.make("agents", {}, () =>
-  Effect.gen(function* () {
-    const svc = {
-      plugin: yield* PluginBoot.Service,
-      agent: yield* AgentV2.Service,
-    }
-    yield* svc.plugin.wait()
-    const agents = yield* svc.agent.all()
-    process.stdout.write(
-      JSON.stringify(
-        agents.sort((a, b) => a.id.localeCompare(b.id)),
-        null,
-        2,
-      ) + EOL,
-    )
-  }).pipe(
-    Effect.provide(
-      LocationServiceMap.get({
-        directory: AbsolutePath.make(process.cwd()),
-      }),
-    ),
-  ),
-).pipe(Command.withDescription("List all agents"))

+ 0 - 7
packages/cli/src/debug/index.ts

@@ -1,7 +0,0 @@
-import * as Command from "effect/unstable/cli/Command"
-import { AgentsCommand } from "./agents"
-
-export const DebugCommand = Command.make("debug").pipe(
-  Command.withDescription("Debugging and troubleshooting tools"),
-  Command.withSubcommands([AgentsCommand]),
-)

+ 17 - 0
packages/cli/src/handlers/debug/agents.ts

@@ -0,0 +1,17 @@
+import { EOL } from "os"
+import { AgentV2 } from "@opencode-ai/core/agent"
+import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { PluginBoot } from "@opencode-ai/core/plugin/boot"
+import { AbsolutePath } from "@opencode-ai/core/schema"
+import * as Effect from "effect/Effect"
+import { Api } from "../../api"
+import { CliBuilder } from "../../cli-builder"
+
+export default CliBuilder.handler(Api.commands.debug.commands.agents, Effect.fn("cli.debug.agents")(function* () {
+  const svc = {
+    plugin: yield* PluginBoot.Service,
+    agent: yield* AgentV2.Service,
+  }
+  yield* svc.plugin.wait()
+  process.stdout.write(JSON.stringify((yield* svc.agent.all()).sort((a, b) => a.id.localeCompare(b.id)), null, 2) + EOL)
+}, Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(process.cwd()) })), Effect.provide(LocationServiceMap.layer)))

+ 5 - 0
packages/cli/src/handlers/migrate.ts

@@ -0,0 +1,5 @@
+import * as Effect from "effect/Effect"
+import { Api } from "../api"
+import { CliBuilder } from "../cli-builder"
+
+export default CliBuilder.handler(Api.commands.migrate, (_input) => Effect.log("No migrations to run."))

+ 13 - 11
packages/cli/src/index.ts

@@ -3,16 +3,18 @@
 import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
 import * as NodeServices from "@effect/platform-node/NodeServices"
 import * as Effect from "effect/Effect"
-import * as Layer from "effect/Layer"
-import * as Command from "effect/unstable/cli/Command"
-import { DebugCommand } from "./debug"
-import { LocationServiceMap } from "@opencode-ai/core/location-layer"
+import { Api } from "./api"
+import { CliBuilder } from "./cli-builder"
 
-const cli = Command.make("opencode", {}, () => Effect.void).pipe(
-  Command.withDescription("OpenCode command line interface"),
-  Command.withSubcommands([DebugCommand]),
-)
-
-const layer = Layer.mergeAll(LocationServiceMap.layer, NodeServices.layer)
+const Handlers = CliBuilder.handlers(Api, {
+  debug: {
+    agents: () => import("./handlers/debug/agents"),
+  },
+  migrate: () => import("./handlers/migrate"),
+})
 
-Command.run(cli, { version: "local" }).pipe(Effect.provide(layer), Effect.scoped, NodeRuntime.runMain)
+CliBuilder.run(Api, Handlers, { version: "local" }).pipe(
+  Effect.provide(NodeServices.layer),
+  Effect.scoped,
+  NodeRuntime.runMain,
+)

+ 16 - 3
packages/core/src/config.ts

@@ -21,6 +21,8 @@ import { ConfigProvider } from "./config/provider"
 import { ConfigReference } from "./config/reference"
 import { ConfigToolOutput } from "./config/tool-output"
 import { ConfigWatcher } from "./config/watcher"
+import { ConfigV1 } from "./v1/config/config"
+import { ConfigMigrateV1 } from "./v1/config/migrate"
 
 export class Info extends Schema.Class<Info>("Config.Info")({
   $schema: Schema.optional(Schema.String).annotate({
@@ -141,10 +143,21 @@ export const layer = Layer.effect(
       const input: unknown = parse(text, errors, { allowTrailingComma: true })
       if (errors.length) return
 
-      // Accept legacy fields while v2 is migrated incrementally; recognized
-      // fields still have to satisfy the v2 schema.
+      const decoded = ConfigMigrateV1.isV1(input)
+        ? Option.map(
+            Schema.decodeUnknownOption(ConfigV1.Info)(input, {
+              errors: "all",
+              onExcessProperty: "ignore",
+              propertyOrder: "original",
+            }),
+            ConfigMigrateV1.migrate,
+          )
+        : Option.some(input)
       const info = Option.getOrUndefined(
-        Schema.decodeUnknownOption(Info)(input, { errors: "all", onExcessProperty: "ignore" }),
+        Option.flatMap(
+          decoded,
+          Schema.decodeUnknownOption(Info, { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" }),
+        ),
       )
       if (!info) return
       return new Loaded({ source: { type: "file", path: filepath }, info })

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

@@ -5,7 +5,7 @@ import { DateTime, Effect, Layer, Schema } from "effect"
 import { Database } from "../database/database"
 import { EventV2 } from "../event"
 import { SessionEvent } from "./event"
-import { SessionLegacy } from "./legacy"
+import { SessionV1 } from "../v1/session"
 import { WorkspaceTable } from "../control-plane/workspace.sql"
 import { SessionMessage } from "./message"
 import { SessionMessageUpdater } from "./message-updater"
@@ -27,7 +27,7 @@ type Usage = {
   }
 }
 
-function usage(part: (typeof SessionLegacy.Event.PartUpdated.Type)["data"]["part"] | unknown): Usage | undefined {
+function usage(part: (typeof SessionV1.Event.PartUpdated.Type)["data"]["part"] | unknown): Usage | undefined {
   if (typeof part !== "object" || part === null) return undefined
   const value = part as Record<string, unknown>
   if (value.type !== "step-finish") return undefined
@@ -35,7 +35,7 @@ function usage(part: (typeof SessionLegacy.Event.PartUpdated.Type)["data"]["part
   return { cost: value.cost as Usage["cost"], tokens: value.tokens as Usage["tokens"] }
 }
 
-function sessionRow(info: SessionLegacy.SessionInfo): typeof SessionTable.$inferInsert {
+function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInsert {
   return {
     id: info.id,
     project_id: info.projectID,
@@ -70,14 +70,14 @@ function sessionRow(info: SessionLegacy.SessionInfo): typeof SessionTable.$infer
 }
 
 function messageData(
-  info: (typeof SessionLegacy.Event.MessageUpdated.Type)["data"]["info"],
+  info: (typeof SessionV1.Event.MessageUpdated.Type)["data"]["info"],
 ): typeof MessageTable.$inferInsert.data {
   const { id: _, sessionID: __, ...rest } = info
   return rest as DeepMutable<typeof rest>
 }
 
 function partData(
-  part: (typeof SessionLegacy.Event.PartUpdated.Type)["data"]["part"],
+  part: (typeof SessionV1.Event.PartUpdated.Type)["data"]["part"],
 ): typeof PartTable.$inferInsert.data {
   const { id: _, messageID: __, sessionID: ___, ...rest } = part
   return rest as DeepMutable<typeof rest>
@@ -85,7 +85,7 @@ function partData(
 
 function applyUsage(
   db: DatabaseService,
-  sessionID: (typeof SessionLegacy.Event.MessageUpdated.Type)["data"]["sessionID"],
+  sessionID: (typeof SessionV1.Event.MessageUpdated.Type)["data"]["sessionID"],
   value: Usage,
   sign = 1,
 ) {
@@ -270,7 +270,7 @@ export const layer = Layer.effectDiscard(
   Effect.gen(function* () {
     const events = yield* EventV2.Service
     const { db } = yield* Database.Service
-    yield* events.project(SessionLegacy.Event.Created, (event) =>
+    yield* events.project(SessionV1.Event.Created, (event) =>
       Effect.gen(function* () {
         yield* db.insert(SessionTable).values(sessionRow(event.data.info)).run().pipe(Effect.orDie)
         if (event.data.info.workspaceID) {
@@ -283,7 +283,7 @@ export const layer = Layer.effectDiscard(
         }
       }),
     )
-    yield* events.project(SessionLegacy.Event.Updated, (event) =>
+    yield* events.project(SessionV1.Event.Updated, (event) =>
       db
         .update(SessionTable)
         .set(sessionRow(event.data.info))
@@ -291,10 +291,10 @@ export const layer = Layer.effectDiscard(
         .run()
         .pipe(Effect.orDie),
     )
-    yield* events.project(SessionLegacy.Event.Deleted, (event) =>
+    yield* events.project(SessionV1.Event.Deleted, (event) =>
       db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie),
     )
-    yield* events.project(SessionLegacy.Event.MessageUpdated, (event) =>
+    yield* events.project(SessionV1.Event.MessageUpdated, (event) =>
       Effect.gen(function* () {
         const time_created = event.data.info.time.created
         const id = event.data.info.id
@@ -308,7 +308,7 @@ export const layer = Layer.effectDiscard(
           .pipe(Effect.orDie)
       }),
     )
-    yield* events.project(SessionLegacy.Event.MessageRemoved, (event) =>
+    yield* events.project(SessionV1.Event.MessageRemoved, (event) =>
       Effect.gen(function* () {
         const rows = yield* db
           .select()
@@ -327,7 +327,7 @@ export const layer = Layer.effectDiscard(
           .pipe(Effect.orDie)
       }),
     )
-    yield* events.project(SessionLegacy.Event.PartRemoved, (event) =>
+    yield* events.project(SessionV1.Event.PartRemoved, (event) =>
       Effect.gen(function* () {
         const row = yield* db
           .select()
@@ -344,7 +344,7 @@ export const layer = Layer.effectDiscard(
           .pipe(Effect.orDie)
       }),
     )
-    yield* events.project(SessionLegacy.Event.PartUpdated, (event) =>
+    yield* events.project(SessionV1.Event.PartUpdated, (event) =>
       Effect.gen(function* () {
         const id = event.data.part.id
         const messageID = event.data.part.messageID

+ 7 - 7
packages/core/src/session/sql.ts

@@ -3,16 +3,16 @@ import * as DatabasePath from "../database/path"
 import { ProjectTable } from "../project/sql"
 import type { SessionMessage } from "./message"
 import type { Snapshot } from "../snapshot"
-import { PermissionLegacy } from "../permission/legacy"
+import { PermissionV1 } from "../v1/permission"
 import { ProjectV2 } from "../project"
 import type { SessionSchema } from "./schema"
-import type { MessageID, PartID, Info as LegacyMessageInfo, Part as LegacyMessagePart } from "./legacy"
+import type { MessageID, PartID, SessionV1 } from "../v1/session"
 import { WorkspaceV2 } from "../workspace"
 import { Timestamps } from "../database/schema.sql"
 
 type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
-type LegacyMessageData = Omit<LegacyMessageInfo, "id" | "sessionID">
-type LegacyPartData = Omit<LegacyMessagePart, "id" | "sessionID" | "messageID">
+type V1MessageData = Omit<SessionV1.Info, "id" | "sessionID">
+type V1PartData = Omit<SessionV1.Part, "id" | "sessionID" | "messageID">
 
 export const SessionTable = sqliteTable(
   "session",
@@ -42,7 +42,7 @@ export const SessionTable = sqliteTable(
     tokens_cache_read: integer().notNull().default(0),
     tokens_cache_write: integer().notNull().default(0),
     revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(),
-    permission: text({ mode: "json" }).$type<PermissionLegacy.Ruleset>(),
+    permission: text({ mode: "json" }).$type<PermissionV1.Ruleset>(),
     agent: text(),
     model: text({ mode: "json" }).$type<{
       id: string
@@ -69,7 +69,7 @@ export const MessageTable = sqliteTable(
       .notNull()
       .references(() => SessionTable.id, { onDelete: "cascade" }),
     ...Timestamps,
-    data: text({ mode: "json" }).notNull().$type<LegacyMessageData>(),
+    data: text({ mode: "json" }).notNull().$type<V1MessageData>(),
   },
   (table) => [index("message_session_time_created_id_idx").on(table.session_id, table.time_created, table.id)],
 )
@@ -84,7 +84,7 @@ export const PartTable = sqliteTable(
       .references(() => MessageTable.id, { onDelete: "cascade" }),
     session_id: text().$type<SessionSchema.ID>().notNull(),
     ...Timestamps,
-    data: text({ mode: "json" }).notNull().$type<LegacyPartData>(),
+    data: text({ mode: "json" }).notNull().$type<V1PartData>(),
   },
   (table) => [
     index("part_message_id_id_idx").on(table.message_id, table.id),

+ 89 - 0
packages/core/src/v1/config/agent.ts

@@ -0,0 +1,89 @@
+export * as ConfigAgentV1 from "./agent"
+
+import { Schema, SchemaGetter } from "effect"
+import { PositiveInt } from "../../schema"
+import { ConfigPermissionV1 } from "./permission"
+
+const Color = Schema.Union([
+  Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
+  Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
+])
+
+const AgentSchema = Schema.StructWithRest(
+  Schema.Struct({
+    model: Schema.optional(Schema.String),
+    variant: Schema.optional(Schema.String).annotate({
+      description: "Default model variant for this agent (applies only when using the agent's configured model).",
+    }),
+    temperature: Schema.optional(Schema.Finite),
+    top_p: Schema.optional(Schema.Finite),
+    prompt: Schema.optional(Schema.String),
+    tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
+      description: "@deprecated Use 'permission' field instead",
+    }),
+    disable: Schema.optional(Schema.Boolean),
+    description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
+    mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
+    hidden: Schema.optional(Schema.Boolean).annotate({
+      description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
+    }),
+    options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
+    color: Schema.optional(Color).annotate({
+      description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
+    }),
+    steps: Schema.optional(PositiveInt).annotate({
+      description: "Maximum number of agentic iterations before forcing text-only response",
+    }),
+    maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
+    permission: Schema.optional(ConfigPermissionV1.Info),
+  }),
+  [Schema.Record(Schema.String, Schema.Any)],
+)
+
+const KNOWN_KEYS = new Set([
+  "name",
+  "model",
+  "variant",
+  "prompt",
+  "description",
+  "temperature",
+  "top_p",
+  "mode",
+  "hidden",
+  "color",
+  "steps",
+  "maxSteps",
+  "options",
+  "permission",
+  "disable",
+  "tools",
+])
+
+const normalize = (agent: Schema.Schema.Type<typeof AgentSchema>): Schema.Schema.Type<typeof AgentSchema> => {
+  const options: Record<string, unknown> = { ...agent.options }
+  for (const [key, value] of Object.entries(agent)) {
+    if (!KNOWN_KEYS.has(key)) options[key] = value
+  }
+
+  const permission: ConfigPermissionV1.Info = {}
+  for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
+    const action = enabled ? "allow" : "deny"
+    if (tool === "write" || tool === "edit" || tool === "patch") {
+      permission.edit = action
+      continue
+    }
+    permission[tool] = action
+  }
+  globalThis.Object.assign(permission, agent.permission)
+
+  const steps = agent.steps ?? agent.maxSteps
+  return { ...agent, options, permission, ...(steps !== undefined ? { steps } : {}) }
+}
+
+export const Info = AgentSchema.pipe(
+  Schema.decodeTo(AgentSchema, {
+    decode: SchemaGetter.transform(normalize),
+    encode: SchemaGetter.passthrough({ strict: false }),
+  }),
+).annotate({ identifier: "AgentConfig" })
+export type Info = Schema.Schema.Type<typeof Info>

+ 2 - 2
packages/opencode/src/config/attachment.ts → packages/core/src/v1/config/attachment.ts

@@ -1,7 +1,7 @@
-export * as ConfigAttachment from "./attachment"
+export * as ConfigAttachmentV1 from "./attachment"
 
 import { Schema } from "effect"
-import { PositiveInt } from "@opencode-ai/core/schema"
+import { PositiveInt } from "../../schema"
 
 export const Image = Schema.Struct({
   auto_resize: Schema.optional(Schema.Boolean).annotate({

+ 12 - 0
packages/core/src/v1/config/command.ts

@@ -0,0 +1,12 @@
+export * as ConfigCommandV1 from "./command"
+
+import { Schema } from "effect"
+
+export const Info = Schema.Struct({
+  template: Schema.String,
+  description: Schema.optional(Schema.String),
+  agent: Schema.optional(Schema.String),
+  model: Schema.optional(Schema.String),
+  subtask: Schema.optional(Schema.Boolean),
+})
+export type Info = Schema.Schema.Type<typeof Info>

+ 69 - 0
packages/core/src/v1/config/config.ts

@@ -0,0 +1,69 @@
+export * as ConfigV1 from "./config"
+
+import { Schema } from "effect"
+import { NonNegativeInt, PositiveInt, type DeepMutable } from "../../schema"
+import { ConfigExperimental } from "../../config/experimental"
+import { ConfigAgentV1 } from "./agent"
+import { ConfigAttachmentV1 } from "./attachment"
+import { ConfigCommandV1 } from "./command"
+import { ConfigFormatterV1 } from "./formatter"
+import { ConfigLayoutV1 } from "./layout"
+import { ConfigLSPV1 } from "./lsp"
+import { ConfigMCPV1 } from "./mcp"
+import { ConfigPermissionV1 } from "./permission"
+import { ConfigPluginV1 } from "./plugin"
+import { ConfigProviderV1 } from "./provider"
+import { ConfigReferenceV1 } from "./reference"
+import { ConfigServerV1 } from "./server"
+import { ConfigSkillsV1 } from "./skills"
+
+export type Layout = ConfigLayoutV1.Layout
+
+export const WellKnown = Schema.Struct({
+  config: Schema.optional(Schema.Json),
+  remote_config: Schema.optional(Schema.Json),
+})
+
+const LogLevelRef = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
+  identifier: "LogLevel",
+  description: "Log level",
+})
+
+export const Info = Schema.Struct({
+  $schema: Schema.optional(Schema.String).annotate({ description: "JSON schema reference for configuration validation" }),
+  shell: Schema.optional(Schema.String).annotate({ description: "Default shell to use for terminal and bash tool" }),
+  logLevel: Schema.optional(LogLevelRef).annotate({ description: "Log level" }),
+  server: Schema.optional(ConfigServerV1.Server).annotate({ description: "Server configuration for opencode serve and web commands" }),
+  command: Schema.optional(Schema.Record(Schema.String, ConfigCommandV1.Info)).annotate({ description: "Command configuration, see https://opencode.ai/docs/commands" }),
+  skills: Schema.optional(ConfigSkillsV1.Info).annotate({ description: "Additional skill folder paths" }),
+  reference: Schema.optional(ConfigReferenceV1.Info).annotate({ description: "Named git or local directory references that can be mentioned as @alias or @alias/path" }),
+  watcher: Schema.optional(Schema.Struct({ ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))) })),
+  snapshot: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true." }),
+  plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPluginV1.Spec))),
+  share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({ description: "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing" }),
+  autoshare: Schema.optional(Schema.Boolean).annotate({ description: "@deprecated Use 'share' field instead. Share newly created sessions automatically" }),
+  autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({ description: "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications" }),
+  disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ description: "Disable providers that are loaded automatically" }),
+  enabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ description: "When set, ONLY these providers will be enabled. All other providers will be ignored" }),
+  model: Schema.optional(Schema.String).annotate({ description: "Model to use in the format of provider/model, eg anthropic/claude-2" }),
+  small_model: Schema.optional(Schema.String).annotate({ description: "Small model to use for tasks like title generation in the format of provider/model" }),
+  default_agent: Schema.optional(Schema.String).annotate({ description: "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid." }),
+  username: Schema.optional(Schema.String).annotate({ description: "Custom username to display in conversations instead of system username" }),
+  mode: Schema.optional(Schema.StructWithRest(Schema.Struct({ build: Schema.optional(ConfigAgentV1.Info), plan: Schema.optional(ConfigAgentV1.Info) }), [Schema.Record(Schema.String, ConfigAgentV1.Info)])).annotate({ description: "@deprecated Use `agent` field instead." }),
+  agent: Schema.optional(Schema.StructWithRest(Schema.Struct({ plan: Schema.optional(ConfigAgentV1.Info), build: Schema.optional(ConfigAgentV1.Info), general: Schema.optional(ConfigAgentV1.Info), explore: Schema.optional(ConfigAgentV1.Info), title: Schema.optional(ConfigAgentV1.Info), summary: Schema.optional(ConfigAgentV1.Info), compaction: Schema.optional(ConfigAgentV1.Info) }), [Schema.Record(Schema.String, ConfigAgentV1.Info)])).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }),
+  provider: Schema.optional(Schema.Record(Schema.String, ConfigProviderV1.Info)).annotate({ description: "Custom provider configurations and model overrides" }),
+  mcp: Schema.optional(Schema.Record(Schema.String, Schema.Union([ConfigMCPV1.Info, Schema.Struct({ enabled: Schema.Boolean })]))).annotate({ description: "MCP (Model Context Protocol) server configurations" }),
+  formatter: Schema.optional(ConfigFormatterV1.Info).annotate({ description: "Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides." }),
+  lsp: Schema.optional(ConfigLSPV1.Info).annotate({ description: "Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides." }),
+  instructions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ description: "Additional instruction files or patterns to include" }),
+  layout: Schema.optional(ConfigLayoutV1.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
+  permission: Schema.optional(ConfigPermissionV1.Info),
+  tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
+  attachment: Schema.optional(ConfigAttachmentV1.Info).annotate({ description: "Attachment processing configuration, including image size limits and resizing behavior" }),
+  enterprise: Schema.optional(Schema.Struct({ url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }) })),
+  tool_output: Schema.optional(Schema.Struct({ max_lines: Schema.optional(PositiveInt).annotate({ description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)" }), max_bytes: Schema.optional(PositiveInt).annotate({ description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)" }) })).annotate({ description: "Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned." }),
+  compaction: Schema.optional(Schema.Struct({ auto: Schema.optional(Schema.Boolean).annotate({ description: "Enable automatic compaction when context is full (default: true)" }), prune: Schema.optional(Schema.Boolean).annotate({ description: "Enable pruning of old tool outputs (default: true)" }), tail_turns: Schema.optional(NonNegativeInt).annotate({ description: "Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)" }), preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({ description: "Maximum number of tokens from recent turns to preserve verbatim after compaction" }), reserved: Schema.optional(NonNegativeInt).annotate({ description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction." }) })),
+  experimental: Schema.optional(Schema.Struct({ disable_paste_summary: Schema.optional(Schema.Boolean), batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }), openTelemetry: Schema.optional(Schema.Boolean).annotate({ description: "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)" }), primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ description: "Tools that should only be available to primary agents." }), continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({ description: "Continue the agent loop when a tool call is denied" }), mcp_timeout: Schema.optional(PositiveInt).annotate({ description: "Timeout in milliseconds for model context protocol (MCP) requests" }), policies: Schema.optional(Schema.mutable(Schema.Array(ConfigExperimental.Policy))).annotate({ description: "Policy statements applied to supported resources, such as provider access" }) })),
+}).annotate({ identifier: "Config" })
+
+export type Info = DeepMutable<Schema.Schema.Type<typeof Info>>

+ 3 - 1
packages/opencode/src/config/console-state.ts → packages/core/src/v1/config/console-state.ts

@@ -1,5 +1,7 @@
+export * as ConfigConsoleStateV1 from "./console-state"
+
 import { Schema } from "effect"
-import { NonNegativeInt } from "@opencode-ai/core/schema"
+import { NonNegativeInt } from "../../schema"
 
 export class ConsoleState extends Schema.Class<ConsoleState>("ConsoleState")({
   consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),

+ 13 - 2
packages/opencode/src/config/error.ts → packages/core/src/v1/config/error.ts

@@ -1,7 +1,7 @@
-export * as ConfigError from "./error"
+export * as ConfigErrorV1 from "./error"
 
-import { NamedError } from "@opencode-ai/core/util/error"
 import { Schema } from "effect"
+import { NamedError } from "../../util/error"
 
 const Issue = Schema.StructWithRest(
   Schema.Struct({
@@ -21,3 +21,14 @@ export const InvalidError = NamedError.create("ConfigInvalidError", {
   issues: Schema.optional(Schema.Array(Issue)),
   message: Schema.optional(Schema.String),
 })
+
+export const FrontmatterError = NamedError.create("ConfigFrontmatterError", {
+  path: Schema.String,
+  message: Schema.String,
+})
+
+export const DirectoryTypoError = NamedError.create("ConfigDirectoryTypoError", {
+  path: Schema.String,
+  dir: Schema.String,
+  suggestion: Schema.String,
+})

+ 1 - 1
packages/opencode/src/config/formatter.ts → packages/core/src/v1/config/formatter.ts

@@ -1,4 +1,4 @@
-export * as ConfigFormatter from "./formatter"
+export * as ConfigFormatterV1 from "./formatter"
 
 import { Schema } from "effect"
 

+ 2 - 2
packages/opencode/src/config/layout.ts → packages/core/src/v1/config/layout.ts

@@ -1,6 +1,6 @@
+export * as ConfigLayoutV1 from "./layout"
+
 import { Schema } from "effect"
 
 export const Layout = Schema.Literals(["auto", "stretch"]).annotate({ identifier: "LayoutConfig" })
 export type Layout = Schema.Schema.Type<typeof Layout>
-
-export * as ConfigLayout from "./layout"

+ 46 - 9
packages/opencode/src/config/lsp.ts → packages/core/src/v1/config/lsp.ts

@@ -1,7 +1,6 @@
-export * as ConfigLSP from "./lsp"
+export * as ConfigLSPV1 from "./lsp"
 
 import { Schema } from "effect"
-import * as LSPServer from "../lsp/server"
 
 export const Disabled = Schema.Struct({
   disabled: Schema.Literal(true),
@@ -18,19 +17,57 @@ export const Entry = Schema.Union([
   }),
 ]).pipe((schema) => schema)
 
-/**
- * For custom (non-builtin) LSP server entries, `extensions` is required so the
- * client knows which files the server should attach to. Builtin server IDs and
- * explicitly disabled entries are exempt.
- */
+// Keep this list aligned with the builtin servers in opencode's LSP runtime.
+// Custom servers must declare extensions because the runtime cannot infer them.
+export const builtinServerIds = [
+  "deno",
+  "typescript",
+  "vue",
+  "eslint",
+  "oxlint",
+  "biome",
+  "gopls",
+  "ruby-lsp",
+  "ty",
+  "pyright",
+  "elixir-ls",
+  "zls",
+  "csharp",
+  "razor",
+  "fsharp",
+  "sourcekit-lsp",
+  "rust",
+  "clangd",
+  "svelte",
+  "astro",
+  "jdtls",
+  "kotlin-ls",
+  "yaml-ls",
+  "lua-ls",
+  "php intelephense",
+  "prisma",
+  "dart",
+  "ocaml-lsp",
+  "bash",
+  "terraform",
+  "texlab",
+  "dockerfile",
+  "gleam",
+  "clojure-lsp",
+  "nixd",
+  "tinymist",
+  "haskell-language-server",
+  "julials",
+]
+
 export const requiresExtensionsForCustomServers = Schema.makeFilter<
   boolean | Record<string, Schema.Schema.Type<typeof Entry>>
 >((data) => {
   if (typeof data === "boolean") return undefined
-  const serverIds = new Set(Object.values(LSPServer).map((server) => server.id))
+  const ids = new Set(builtinServerIds)
   const ok = Object.entries(data).every(([id, config]) => {
     if ("disabled" in config && config.disabled) return true
-    if (serverIds.has(id)) return true
+    if (ids.has(id)) return true
     return "extensions" in config && Boolean(config.extensions)
   })
   return ok ? undefined : "For custom LSP servers, 'extensions' array is required."

+ 3 - 3
packages/opencode/src/config/mcp.ts → packages/core/src/v1/config/mcp.ts

@@ -1,5 +1,7 @@
+export * as ConfigMCPV1 from "./mcp"
+
 import { Schema } from "effect"
-import { PositiveInt } from "@opencode-ai/core/schema"
+import { PositiveInt } from "../../schema"
 
 export const Local = Schema.Struct({
   type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }),
@@ -56,5 +58,3 @@ export type Remote = Schema.Schema.Type<typeof Remote>
 
 export const Info = Schema.Union([Local, Remote]).annotate({ discriminator: "type" })
 export type Info = Schema.Schema.Type<typeof Info>
-
-export * as ConfigMCP from "./mcp"

+ 212 - 0
packages/core/src/v1/config/migrate.ts

@@ -0,0 +1,212 @@
+export * as ConfigMigrateV1 from "./migrate"
+
+import { ConfigV1 } from "./config"
+import { ConfigAgentV1 } from "./agent"
+import { ConfigMCPV1 } from "./mcp"
+import { ConfigPermissionV1 } from "./permission"
+import { ConfigProviderV1 } from "./provider"
+
+const keys = new Set([
+  "logLevel",
+  "server",
+  "command",
+  "reference",
+  "snapshot",
+  "plugin",
+  "autoshare",
+  "disabled_providers",
+  "enabled_providers",
+  "small_model",
+  "default_agent",
+  "mode",
+  "agent",
+  "provider",
+  "permission",
+  "tools",
+  "attachment",
+  "layout",
+])
+
+export function isV1(input: unknown) {
+  if (typeof input !== "object" || input === null || Array.isArray(input)) return false
+  return Object.keys(input).some((key) => keys.has(key))
+}
+
+export function migrate(info: typeof ConfigV1.Info.Type) {
+  return {
+    $schema: info.$schema,
+    shell: info.shell,
+    model: info.model,
+    autoupdate: info.autoupdate,
+    share: info.share ?? (info.autoshare ? "auto" : undefined),
+    enterprise: info.enterprise,
+    username: info.username,
+    permissions: permissions(info.permission, info.tools),
+    agents: agents(info),
+    snapshots: info.snapshot,
+    watcher: info.watcher,
+    formatter: info.formatter,
+    lsp: info.lsp,
+    attachments: info.attachment,
+    tool_output: info.tool_output,
+    mcp: mcp(info),
+    compaction: info.compaction && {
+      auto: info.compaction.auto,
+      prune: info.compaction.prune,
+      keep: {
+        turns: info.compaction.tail_turns,
+        tokens: info.compaction.preserve_recent_tokens,
+      },
+      buffer: info.compaction.reserved,
+    },
+    skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])],
+    instructions: info.instructions,
+    references: info.reference,
+    plugins: info.plugin?.map((plugin) =>
+      typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] },
+    ),
+    experimental: info.experimental?.policies && { policies: info.experimental.policies },
+    providers: providers(info.provider),
+  }
+}
+
+function permissions(info?: ConfigPermissionV1.Info, tools?: Readonly<Record<string, boolean>>) {
+  const rules: Array<{ action: string; resource: string; effect: ConfigPermissionV1.Action }> = Object.entries(
+    tools ?? {},
+  ).map(([action, enabled]) => ({
+    action: normalizeAction(action),
+    resource: "*",
+    effect: enabled ? ("allow" as const) : ("deny" as const),
+  }))
+  for (const [action, rule] of Object.entries(info ?? {})) {
+    if (!rule) continue
+    if (typeof rule === "string") {
+      rules.push({ action, resource: "*", effect: rule })
+      continue
+    }
+    rules.push(...Object.entries(rule).map(([resource, effect]) => ({ action, resource, effect })))
+  }
+  return rules.length ? rules : undefined
+}
+
+function normalizeAction(action: string) {
+  return action === "write" || action === "patch" ? "edit" : action
+}
+
+function agents(info: typeof ConfigV1.Info.Type) {
+  const entries = [
+    ...Object.entries(info.agent ?? {}),
+    ...Object.entries(info.mode ?? {}).map(([name, agent]) => [name, { ...agent, mode: "primary" as const }] as const),
+  ]
+  if (!entries.length) return undefined
+  return Object.fromEntries(entries.map(([name, agent]) => [name, migrateAgent(agent)]))
+}
+
+function migrateAgent(info: ConfigAgentV1.Info) {
+  const body = {
+    ...info.options,
+    ...(info.temperature === undefined ? {} : { temperature: info.temperature }),
+    ...(info.top_p === undefined ? {} : { top_p: info.top_p }),
+  }
+  return {
+    model: info.model,
+    variant: info.variant,
+    options: Object.keys(body).length ? { body } : undefined,
+    system: info.prompt,
+    description: info.description,
+    mode: info.mode,
+    hidden: info.hidden,
+    color: info.color,
+    steps: info.steps,
+    disabled: info.disable,
+    permissions: permissions(info.permission),
+  }
+}
+
+function mcp(info: typeof ConfigV1.Info.Type) {
+  const servers = Object.fromEntries(
+    Object.entries(info.mcp ?? {}).flatMap(([name, server]) =>
+      "type" in server ? [[name, migrateMcp(server)] as const] : [],
+    ),
+  )
+  const timeout = info.experimental?.mcp_timeout
+  if (!timeout && !Object.keys(servers).length) return undefined
+  return { timeout, servers }
+}
+
+function migrateMcp(info: ConfigMCPV1.Info) {
+  const disabled = info.enabled === undefined ? undefined : !info.enabled
+  if (info.type === "local") return { type: info.type, command: info.command, environment: info.environment, disabled, timeout: info.timeout }
+  return {
+    type: info.type,
+    url: info.url,
+    headers: info.headers,
+    oauth:
+      info.oauth && {
+        client_id: info.oauth.clientId,
+        client_secret: info.oauth.clientSecret,
+        scope: info.oauth.scope,
+        callback_port: info.oauth.callbackPort,
+        redirect_uri: info.oauth.redirectUri,
+      },
+    disabled,
+    timeout: info.timeout,
+  }
+}
+
+function providers(info?: Readonly<Record<string, ConfigProviderV1.Info>>) {
+  if (!info) return undefined
+  return Object.fromEntries(Object.entries(info).map(([name, provider]) => [name, migrateProvider(provider)]))
+}
+
+function migrateProvider(info: ConfigProviderV1.Info) {
+  return {
+    name: info.name,
+    env: info.env,
+    endpoint: info.npm && {
+      type: "aisdk" as const,
+      package: info.npm,
+      url: info.api ?? (typeof info.options?.baseURL === "string" ? info.options.baseURL : undefined),
+    },
+    options: info.options && { body: info.options },
+    models: info.models && Object.fromEntries(Object.entries(info.models).map(([name, model]) => [name, migrateModel(model)])),
+  }
+}
+
+function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
+  const costs = info.cost && [
+    {
+      input: info.cost.input,
+      output: info.cost.output,
+      cache: { read: info.cost.cache_read, write: info.cost.cache_write },
+    },
+    ...(info.cost.context_over_200k
+      ? [
+          {
+            tier: { type: "context" as const, size: 200_000 },
+            input: info.cost.context_over_200k.input,
+            output: info.cost.context_over_200k.output,
+            cache: { read: info.cost.context_over_200k.cache_read, write: info.cost.context_over_200k.cache_write },
+          },
+        ]
+      : []),
+  ]
+  const capabilities =
+    info.tool_call !== undefined || info.modalities?.input !== undefined || info.modalities?.output !== undefined
+      ? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
+      : undefined
+  return {
+    api_id: info.id,
+    family: info.family,
+    name: info.name,
+    endpoint: info.provider?.npm && { type: "aisdk" as const, package: info.provider.npm, url: info.provider.api },
+    capabilities,
+    options: (info.headers || info.options) && { headers: info.headers, body: info.options },
+    variants:
+      info.variants &&
+      Object.entries(info.variants).map(([id, options]) => ({ id, body: options })),
+    cost: costs,
+    disabled: info.status === "deprecated" ? true : undefined,
+    limit: info.limit,
+  }
+}

+ 2 - 8
packages/opencode/src/config/permission.ts → packages/core/src/v1/config/permission.ts

@@ -1,4 +1,5 @@
-export * as ConfigPermission from "./permission"
+export * as ConfigPermissionV1 from "./permission"
+
 import { Schema, SchemaGetter } from "effect"
 
 export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" })
@@ -34,21 +35,14 @@ const InputObject = Schema.StructWithRest(
   [Schema.Record(Schema.String, Rule)],
 )
 
-// Input the user writes in config: either a single Action (shorthand for "*")
-// or an object of per-target rules.
 const InputSchema = Schema.Union([Action, InputObject])
 
-// Normalise the Action shorthand into `{ "*": action }`. Object inputs pass
-// through untouched.
 const normalizeInput = (input: Schema.Schema.Type<typeof InputSchema>): Schema.Schema.Type<typeof InputObject> =>
   typeof input === "string" ? { "*": input } : input
 
 export const Info = InputSchema.pipe(
   Schema.decodeTo(InputObject, {
     decode: SchemaGetter.transform(normalizeInput),
-    // Not perfectly invertible (we lose whether the user originally typed an
-    // Action shorthand), but the object form is always a valid representation
-    // of the same rules.
     encode: SchemaGetter.passthrough({ strict: false }),
   }),
 ).annotate({ identifier: "PermissionConfig" })

+ 9 - 0
packages/core/src/v1/config/plugin.ts

@@ -0,0 +1,9 @@
+export * as ConfigPluginV1 from "./plugin"
+
+import { Schema } from "effect"
+
+export const Options = Schema.Record(Schema.String, Schema.Unknown)
+export type Options = Schema.Schema.Type<typeof Options>
+
+export const Spec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, Options]))])
+export type Spec = Schema.Schema.Type<typeof Spec>

+ 6 - 7
packages/opencode/src/config/provider.ts → packages/core/src/v1/config/provider.ts

@@ -1,6 +1,9 @@
+export * as ConfigProviderV1 from "./provider"
+
 import { Schema } from "effect"
-import { PositiveInt } from "@opencode-ai/core/schema"
-import { ModelStatus } from "@/provider/model-status"
+import { PositiveInt } from "../../schema"
+
+export const ModelStatus = Schema.Literals(["alpha", "beta", "deprecated", "active"])
 
 export const Model = Schema.Struct({
   id: Schema.optional(Schema.String),
@@ -52,9 +55,7 @@ export const Model = Schema.Struct({
   ),
   experimental: Schema.optional(Schema.Boolean),
   status: Schema.optional(ModelStatus),
-  provider: Schema.optional(
-    Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
-  ),
+  provider: Schema.optional(Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) })),
   options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
   headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
   variants: Schema.optional(
@@ -116,5 +117,3 @@ export const Info = Schema.Struct({
   models: Schema.optional(Schema.Record(Schema.String, Model)),
 }).annotate({ identifier: "ProviderConfig" })
 export type Info = Schema.Schema.Type<typeof Info>
-
-export * as ConfigProvider from "./provider"

+ 24 - 0
packages/core/src/v1/config/reference.ts

@@ -0,0 +1,24 @@
+export * as ConfigReferenceV1 from "./reference"
+
+import { Schema } from "effect"
+
+const Git = Schema.Struct({
+  repository: Schema.String.annotate({
+    description: "Git repository URL, host/path reference, or GitHub owner/repo shorthand",
+  }),
+  branch: Schema.optional(Schema.String).annotate({
+    description: "Branch or ref to clone and inspect",
+  }),
+})
+
+const Local = Schema.Struct({
+  path: Schema.String.annotate({
+    description: "Absolute path, ~/ path, or workspace-relative path to a local reference directory",
+  }),
+})
+
+export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
+export type Entry = Schema.Schema.Type<typeof Entry>
+
+export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
+export type Info = Schema.Schema.Type<typeof Info>

+ 3 - 3
packages/opencode/src/config/server.ts → packages/core/src/v1/config/server.ts

@@ -1,5 +1,7 @@
+export * as ConfigServerV1 from "./server"
+
 import { Schema } from "effect"
-import { PositiveInt } from "@opencode-ai/core/schema"
+import { PositiveInt } from "../../schema"
 
 export const Server = Schema.Struct({
   port: Schema.optional(PositiveInt).annotate({
@@ -15,5 +17,3 @@ export const Server = Schema.Struct({
   }),
 }).annotate({ identifier: "ServerConfig" })
 export type Server = Schema.Schema.Type<typeof Server>
-
-export * as ConfigServer from "./server"

+ 2 - 3
packages/opencode/src/config/skills.ts → packages/core/src/v1/config/skills.ts

@@ -1,3 +1,5 @@
+export * as ConfigSkillsV1 from "./skills"
+
 import { Schema } from "effect"
 
 export const Info = Schema.Struct({
@@ -8,7 +10,4 @@ export const Info = Schema.Struct({
     description: "URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)",
   }),
 })
-
 export type Info = Schema.Schema.Type<typeof Info>
-
-export * as ConfigSkills from "./skills"

+ 1 - 1
packages/core/src/permission/legacy.ts → packages/core/src/v1/permission.ts

@@ -1,4 +1,4 @@
-export * as PermissionLegacy from "./legacy"
+export * as PermissionV1 from "./permission"
 
 import { Schema } from "effect"
 import { ProjectV2 } from "../project"

+ 4 - 4
packages/core/src/session/legacy.ts → packages/core/src/v1/session.ts

@@ -1,15 +1,15 @@
-export * as SessionLegacy from "./legacy"
+export * as SessionV1 from "./session"
 
 import { Effect, Schema, Types } from "effect"
 import { EventV2 } from "../event"
-import { PermissionLegacy } from "../permission/legacy"
+import { PermissionV1 } from "./permission"
 import { ProjectV2 } from "../project"
 import { ProviderV2 } from "../provider"
 import { optionalOmitUndefined, withStatics } from "../schema"
 import { Identifier } from "../util/identifier"
 import { NonNegativeInt } from "../schema"
 import { NamedError } from "../util/error"
-import { SessionSchema } from "./schema"
+import { SessionSchema } from "../session/schema"
 import { WorkspaceV2 } from "../workspace"
 
 export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(
@@ -558,7 +558,7 @@ export const SessionInfo = Schema.Struct({
     compacting: optionalOmitUndefined(NonNegativeInt),
     archived: optionalOmitUndefined(Schema.Finite),
   }),
-  permission: optionalOmitUndefined(PermissionLegacy.Ruleset),
+  permission: optionalOmitUndefined(PermissionV1.Ruleset),
   revert: optionalOmitUndefined(SessionRevert),
 }).annotate({ identifier: "Session" })
 export type SessionInfo = typeof SessionInfo.Type

+ 103 - 0
packages/core/test/config/config.test.ts

@@ -4,6 +4,7 @@ import { describe, expect } from "bun:test"
 import { Effect, Layer } from "effect"
 import { Config } from "@opencode-ai/core/config"
 import { ConfigProvider } from "@opencode-ai/core/config/provider"
+import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Global } from "@opencode-ai/core/global"
 import { Location } from "@opencode-ai/core/location"
@@ -53,6 +54,14 @@ const provider = {
 }
 
 describe("Config", () => {
+  it.effect("detects v1 configuration from any v1-only top-level key", () =>
+    Effect.sync(() => {
+      expect(ConfigMigrateV1.isV1({ snapshot: false })).toBe(true)
+      expect(ConfigMigrateV1.isV1({ snapshot: false, agents: {} })).toBe(true)
+      expect(ConfigMigrateV1.isV1({ shell: "/bin/zsh", model: "anthropic/claude" })).toBe(false)
+    }),
+  )
+
   it.live("returns an empty configuration when directory files do not exist", () =>
     Effect.acquireRelease(
       Effect.promise(() => tmpdir()),
@@ -337,6 +346,100 @@ describe("Config", () => {
     ),
   )
 
+  it.live("migrates v1 configuration when a v1-only key is present", () =>
+    Effect.acquireRelease(
+      Effect.promise(() => tmpdir()),
+      (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+    ).pipe(
+      Effect.flatMap((tmp) =>
+        Effect.gen(function* () {
+          yield* Effect.promise(() =>
+            fs.writeFile(
+              path.join(tmp.path, "opencode.json"),
+              JSON.stringify({
+                shell: "/bin/zsh",
+                snapshot: false,
+                autoshare: true,
+                permission: {
+                  bash: "ask",
+                  edit: { "*.md": "allow", "*": "deny" },
+                },
+                agent: {
+                  reviewer: {
+                    prompt: "Review changes.",
+                    disable: true,
+                    temperature: 0.2,
+                    permission: { read: "allow" },
+                  },
+                },
+                plugin: ["opencode-helicone-session", ["@my-org/audit-plugin", { endpoint: "https://audit.example.com" }]],
+                skills: { paths: ["./skills"], urls: ["https://example.com/.well-known/skills/"] },
+                reference: { docs: { path: "../docs" } },
+                attachment: { image: { auto_resize: false, max_width: 1200 } },
+                compaction: { auto: true, tail_turns: 3, preserve_recent_tokens: 2000, reserved: 10000 },
+                experimental: { mcp_timeout: 5000 },
+                mcp: {
+                  local: { type: "local", command: ["node", "server.js"], enabled: false },
+                  remote: {
+                    type: "remote",
+                    url: "https://mcp.example.com",
+                    oauth: { clientId: "client", callbackPort: 19876 },
+                  },
+                },
+              }),
+            ),
+          )
+
+          return yield* Effect.gen(function* () {
+            const config = yield* Config.Service
+            const documents = yield* config.get()
+
+            expect(documents).toHaveLength(1)
+            expect(documents[0]?.info).toBeInstanceOf(Config.Info)
+            expect(documents[0]?.info.shell).toBe("/bin/zsh")
+            expect(documents[0]?.info.snapshots).toBe(false)
+            expect(documents[0]?.info.share).toBe("auto")
+            expect(documents[0]?.info.permissions).toEqual([
+              { action: "bash", resource: "*", effect: "ask" },
+              { action: "edit", resource: "*.md", effect: "allow" },
+              { action: "edit", resource: "*", effect: "deny" },
+            ])
+            expect(documents[0]?.info.agents?.reviewer).toMatchObject({
+              system: "Review changes.",
+              disabled: true,
+              options: { body: { temperature: 0.2 } },
+              permissions: [{ action: "read", resource: "*", effect: "allow" }],
+            })
+            expect(documents[0]?.info.plugins).toEqual([
+              "opencode-helicone-session",
+              { package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } },
+            ])
+            expect(documents[0]?.info.skills).toEqual(["./skills", "https://example.com/.well-known/skills/"])
+            expect(documents[0]?.info.references).toEqual({ docs: { path: "../docs" } })
+            expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } })
+            expect(documents[0]?.info.compaction).toEqual({
+              auto: true,
+              prune: undefined,
+              keep: { turns: 3, tokens: 2000 },
+              buffer: 10000,
+            })
+            expect(documents[0]?.info.mcp).toMatchObject({
+              timeout: 5000,
+              servers: {
+                local: { type: "local", command: ["node", "server.js"], disabled: true },
+                remote: {
+                  type: "remote",
+                  url: "https://mcp.example.com",
+                  oauth: { client_id: "client", callback_port: 19876 },
+                },
+              },
+            })
+          }).pipe(Effect.provide(testLayer(tmp.path)))
+        }),
+      ),
+    ),
+  )
+
   it.live("ignores invalid files while loading valid config values", () =>
     Effect.acquireRelease(
       Effect.promise(() => tmpdir()),

+ 2 - 1
packages/opencode/script/schema.ts

@@ -1,6 +1,7 @@
 #!/usr/bin/env bun
 
 import { Config } from "@/config/config"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { Schema } from "effect"
 import { TuiInfo } from "../src/cli/cmd/tui/config/tui-schema"
 
@@ -68,7 +69,7 @@ const configFile = process.argv[2]
 const tuiFile = process.argv[3]
 
 console.log(configFile)
-await Bun.write(configFile, JSON.stringify(generateEffect(Config.Info), null, 2))
+await Bun.write(configFile, JSON.stringify(generateEffect(ConfigV1.Info), null, 2))
 
 if (tuiFile) {
   console.log(tuiFile)

+ 3 - 3
packages/opencode/src/acp/content.ts

@@ -1,9 +1,9 @@
 import type { ContentBlock, ContentChunk, ResourceLink, Role } from "@agentclientprotocol/sdk"
 import path from "node:path"
 import { pathToFileURL } from "node:url"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 
-export type PromptPart = SessionLegacy.TextPartInput | SessionLegacy.FilePartInput
+export type PromptPart = SessionV1.TextPartInput | SessionV1.FilePartInput
 
 export type ReplayPart =
   | {
@@ -141,7 +141,7 @@ function uriToFilePart(
   uri: string,
   mime: string,
   filename?: string,
-): SessionLegacy.FilePartInput | SessionLegacy.TextPartInput {
+): SessionV1.FilePartInput | SessionV1.TextPartInput {
   try {
     if (uri.startsWith("file://")) {
       return {

+ 2 - 2
packages/opencode/src/agent/agent.ts

@@ -1,4 +1,4 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Config } from "@/config/config"
 import { serviceUse } from "@opencode-ai/core/effect/service-use"
 import { Provider } from "@/provider/provider"
@@ -35,7 +35,7 @@ export const Info = Schema.Struct({
   topP: Schema.optional(Schema.Finite),
   temperature: Schema.optional(Schema.Finite),
   color: Schema.optional(Schema.String),
-  permission: PermissionLegacy.Ruleset,
+  permission: PermissionV1.Ruleset,
   model: Schema.optional(
     Schema.Struct({
       modelID: ProviderV2.ModelID,

+ 3 - 3
packages/opencode/src/agent/subagent-permissions.ts

@@ -1,4 +1,4 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import type { Permission } from "../permission"
 import type { Agent } from "./agent"
 
@@ -16,10 +16,10 @@ import type { Agent } from "./agent"
  *    doesn't already permit them.
  */
 export function deriveSubagentSessionPermission(input: {
-  parentSessionPermission: PermissionLegacy.Ruleset
+  parentSessionPermission: PermissionV1.Ruleset
   parentAgent: Agent.Info | undefined
   subagent: Agent.Info
-}): PermissionLegacy.Ruleset {
+}): PermissionV1.Ruleset {
   const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
   const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
   const parentAgentDenies =

+ 5 - 5
packages/opencode/src/cli/cmd/debug/agent.handler.ts

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { EOL } from "os"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { basename } from "path"
 import { Cause, Effect } from "effect"
 import { Agent } from "../../../agent/agent"
@@ -150,7 +150,7 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio
         )
       })
   const now = Date.now()
-  const message: SessionLegacy.Assistant = {
+  const message: SessionV1.Assistant = {
     id: messageID,
     sessionID: session.id,
     role: "assistant",
@@ -179,12 +179,12 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio
     abort: new AbortController().signal,
     messages: [],
     metadata: () => Effect.void,
-    ask(req: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">) {
+    ask(req: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">) {
       return Effect.sync(() => {
         for (const pattern of req.patterns) {
           const rule = Permission.evaluate(req.permission, pattern, ruleset)
           if (rule.action === "deny") {
-            throw new PermissionLegacy.DeniedError({ ruleset })
+            throw new PermissionV1.DeniedError({ ruleset })
           }
         }
       })

+ 5 - 5
packages/opencode/src/cli/cmd/export.ts

@@ -1,5 +1,5 @@
 import { Session } from "@/session/session"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { MessageV2 } from "../../session/message-v2"
 import { SessionID } from "../../session/schema"
 import { effectCmd, fail } from "../effect-cmd"
@@ -32,7 +32,7 @@ function diff(kind: string, diffs: { file?: string; patch?: string }[] | undefin
   }))
 }
 
-function source(part: SessionLegacy.FilePart) {
+function source(part: SessionV1.FilePart) {
   if (!part.source) return part.source
   if (part.source.type === "symbol") {
     return {
@@ -57,7 +57,7 @@ function source(part: SessionLegacy.FilePart) {
   }
 }
 
-function filepart(part: SessionLegacy.FilePart): SessionLegacy.FilePart {
+function filepart(part: SessionV1.FilePart): SessionV1.FilePart {
   return {
     ...part,
     url: redact("file-url", part.id, part.url),
@@ -66,7 +66,7 @@ function filepart(part: SessionLegacy.FilePart): SessionLegacy.FilePart {
   }
 }
 
-function part(part: SessionLegacy.Part): SessionLegacy.Part {
+function part(part: SessionV1.Part): SessionV1.Part {
   switch (part.type) {
     case "text":
       return {
@@ -160,7 +160,7 @@ function part(part: SessionLegacy.Part): SessionLegacy.Part {
 
 const partFn = part
 
-function sanitize(data: { info: Session.Info; messages: SessionLegacy.WithParts[] }) {
+function sanitize(data: { info: Session.Info; messages: SessionV1.WithParts[] }) {
   return {
     info: {
       ...data.info,

+ 2 - 2
packages/opencode/src/cli/cmd/github.shared.ts

@@ -1,4 +1,4 @@
-import type { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import type { SessionV1 } from "@opencode-ai/core/v1/session"
 
 export { parseGitHubRemote } from "@/util/repository"
 
@@ -7,7 +7,7 @@ export { parseGitHubRemote } from "@/util/repository"
  * Returns null for non-text responses (signals summary needed).
  * Throws only for truly empty responses.
  */
-export function extractResponseText(parts: SessionLegacy.Part[]): string | null {
+export function extractResponseText(parts: SessionV1.Part[]): string | null {
   const textPart = parts.findLast((p) => p.type === "text")
   if (textPart) return textPart.text
 

+ 5 - 5
packages/opencode/src/cli/cmd/import.ts

@@ -1,5 +1,5 @@
 import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Session } from "@/session/session"
 import { MessageV2 } from "../../session/message-v2"
 import { CliError, effectCmd } from "../effect-cmd"
@@ -13,8 +13,8 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Effect, Schema } from "effect"
 import type { InstanceContext } from "@/project/instance-context"
 
-const decodeMessageInfo = Schema.decodeUnknownSync(SessionLegacy.Info)
-const decodePart = Schema.decodeUnknownSync(SessionLegacy.Part)
+const decodeMessageInfo = Schema.decodeUnknownSync(SessionV1.Info)
+const decodePart = Schema.decodeUnknownSync(SessionV1.Part)
 
 /** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
 export type ShareData =
@@ -188,7 +188,7 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: Ins
     .pipe(Effect.orDie)
 
   for (const msg of exportData.messages) {
-    const msgInfo = decodeMessageInfo(msg.info) as SessionLegacy.Info
+    const msgInfo = decodeMessageInfo(msg.info) as SessionV1.Info
     const { id, sessionID: _, ...msgData } = msgInfo
     yield* db
       .insert(MessageTable)
@@ -203,7 +203,7 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: Ins
       .pipe(Effect.orDie)
 
     for (const part of msg.parts) {
-      const partInfo = decodePart(part) as SessionLegacy.Part
+      const partInfo = decodePart(part) as SessionV1.Part
       const { id: partId, sessionID: _s, messageID, ...partData } = partInfo
       yield* db
         .insert(PartTable)

+ 9 - 8
packages/opencode/src/cli/cmd/mcp.ts

@@ -1,4 +1,5 @@
 import { cmd } from "./cmd"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { effectCmd } from "../effect-cmd"
 import { Cause } from "effect"
 import { Client } from "@modelcontextprotocol/sdk/client/index.js"
@@ -10,7 +11,7 @@ import { MCP } from "../../mcp"
 import { McpAuth } from "../../mcp/auth"
 import { McpOAuthProvider } from "../../mcp/oauth-provider"
 import { Config } from "@/config/config"
-import { ConfigMCP } from "../../config/mcp"
+import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
 import { InstanceRef } from "@/effect/instance-ref"
 import { InstallationVersion } from "@opencode-ai/core/installation/version"
 import path from "path"
@@ -43,9 +44,9 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
   }
 }
 
-type McpEntry = NonNullable<Config.Info["mcp"]>[string]
+type McpEntry = NonNullable<ConfigV1.Info["mcp"]>[string]
 
-type McpConfigured = ConfigMCP.Info
+type McpConfigured = ConfigMCPV1.Info
 function isMcpConfigured(config: McpEntry): config is McpConfigured {
   return typeof config === "object" && config !== null && "type" in config
 }
@@ -55,11 +56,11 @@ function isMcpRemote(config: McpEntry): config is McpRemote {
   return isMcpConfigured(config) && config.type === "remote"
 }
 
-function configuredServers(config: Config.Info) {
+function configuredServers(config: ConfigV1.Info) {
   return Object.entries(config.mcp ?? {}).filter((entry): entry is [string, McpConfigured] => isMcpConfigured(entry[1]))
 }
 
-function oauthServers(config: Config.Info) {
+function oauthServers(config: ConfigV1.Info) {
   return configuredServers(config).filter(
     (entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
   )
@@ -418,7 +419,7 @@ async function resolveConfigPath(baseDir: string, global = false) {
   return candidates[0]
 }
 
-async function addMcpToConfig(name: string, mcpConfig: ConfigMCP.Info, configPath: string) {
+async function addMcpToConfig(name: string, mcpConfig: ConfigMCPV1.Info, configPath: string) {
   let text = "{}"
   if (await Filesystem.exists(configPath)) {
     text = await Filesystem.readText(configPath)
@@ -507,7 +508,7 @@ export const McpAddCommand = effectCmd({
         })
         if (prompts.isCancel(command)) throw new UI.CancelledError()
 
-        const mcpConfig: ConfigMCP.Info = {
+        const mcpConfig: ConfigMCPV1.Info = {
           type: "local",
           command: command.split(" "),
         }
@@ -537,7 +538,7 @@ export const McpAddCommand = effectCmd({
         })
         if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
 
-        let mcpConfig: ConfigMCP.Info
+        let mcpConfig: ConfigMCPV1.Info
 
         if (useOAuth) {
           const hasClientId = await prompts.confirm({

+ 2 - 2
packages/opencode/src/cli/cmd/run.ts

@@ -1,4 +1,4 @@
-import type { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
 // CLI entry point for `opencode run`.
 //
 // Handles three modes:
@@ -362,7 +362,7 @@ export const RunCommand = effectCmd({
         process.exit(1)
       }
 
-      const rules: PermissionLegacy.Ruleset = args.interactive
+      const rules: PermissionV1.Ruleset = args.interactive
         ? []
         : [
             {

+ 2 - 2
packages/opencode/src/cli/cmd/tui/config/tui-schema.ts

@@ -1,4 +1,4 @@
-import { ConfigPlugin } from "@/config/plugin"
+import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
 import { TuiKeybind } from "./keybind"
 import { Schema } from "effect"
 import { isRecord } from "@/util/record"
@@ -74,7 +74,7 @@ export const TuiInfo = Schema.Struct({
   $schema: Schema.optional(Schema.String),
   theme: Schema.optional(Schema.String),
   keybinds: Schema.optional(TuiKeybind.KeybindOverrides),
-  plugin: Schema.optional(Schema.Array(ConfigPlugin.Spec)),
+  plugin: Schema.optional(Schema.Array(ConfigPluginV1.Spec)),
   plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
   leader_timeout: Schema.optional(KeymapLeaderTimeout),
   attention: Schema.optional(Attention),

+ 1 - 1
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -29,7 +29,7 @@ import { useExit } from "./exit"
 import { useArgs } from "./args"
 import { batch, onMount } from "solid-js"
 import * as Log from "@opencode-ai/core/util/log"
-import { emptyConsoleState, type ConsoleState } from "@/config/console-state"
+import { emptyConsoleState, type ConsoleState } from "@opencode-ai/core/v1/config/console-state"
 import path from "path"
 import { useKV } from "./kv"
 import { aggregateFailures } from "./aggregate-failures"

+ 3 - 2
packages/opencode/src/cli/cmd/tui/plugin/runtime.ts

@@ -39,6 +39,7 @@ import { internalTuiPlugins, type InternalTuiPlugin } from "./internal"
 import { setupSlots, Slot as View } from "./slots"
 import type { HostPluginApi, HostSlots } from "./slots"
 import { ConfigPlugin } from "@/config/plugin"
+import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
 import { createCommandShim } from "./command-shim"
 import { RuntimeFlags } from "@/effect/runtime-flags"
 import { Effect } from "effect"
@@ -46,7 +47,7 @@ import { Effect } from "effect"
 ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
 
 type PluginLoad = {
-  options: ConfigPlugin.Options | undefined
+  options: ConfigPluginV1.Options | undefined
   spec: string
   target: string
   retry: boolean
@@ -995,7 +996,7 @@ async function installPluginBySpec(
   const tui = manifest.targets.find((item) => item.kind === "tui")
   if (tui) {
     const file = patch.items.find((item) => item.kind === "tui")?.file
-    const next = tui.opts ? ([spec, tui.opts] as ConfigPlugin.Spec) : spec
+    const next = tui.opts ? ([spec, tui.opts] as ConfigPluginV1.Spec) : spec
     state.pending.set(spec, {
       spec: next,
       scope: global ? "global" : "local",

+ 2 - 1
packages/opencode/src/cli/network.ts

@@ -1,4 +1,5 @@
 import type { Argv, InferredOptionTypes } from "yargs"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import type { Config } from "@/config/config"
 import { Effect } from "effect"
 
@@ -42,7 +43,7 @@ export const resolveNetworkOptions = Effect.fn("Cli.resolveNetworkOptions")(func
   return resolveNetworkOptionsNoConfig(args, config)
 })
 
-export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: Config.Info) {
+export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: ConfigV1.Info) {
   const portExplicitlySet = process.argv.includes("--port")
   const hostnameExplicitlySet = process.argv.includes("--hostname")
   const mdnsExplicitlySet = process.argv.includes("--mdns")

+ 6 - 98
packages/opencode/src/config/agent.ts

@@ -1,110 +1,18 @@
 export * as ConfigAgent from "./agent"
 
 import path from "path"
-import { Exit, Schema, SchemaGetter } from "effect"
-import { PositiveInt } from "@opencode-ai/core/schema"
+import { Exit, Schema } from "effect"
 import * as Log from "@opencode-ai/core/util/log"
 import { Glob } from "@opencode-ai/core/util/glob"
+import { ConfigAgentV1 } from "@opencode-ai/core/v1/config/agent"
 import { configEntryNameFromPath } from "./entry-name"
 import * as ConfigMarkdown from "./markdown"
-import { ConfigModelID } from "./model-id"
 import { ConfigParse } from "./parse"
-import { ConfigPermission } from "./permission"
 
 const log = Log.create({ service: "config" })
 
-const Color = Schema.Union([
-  Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
-  Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
-])
-
-const AgentSchema = Schema.StructWithRest(
-  Schema.Struct({
-    model: Schema.optional(ConfigModelID),
-    variant: Schema.optional(Schema.String).annotate({
-      description: "Default model variant for this agent (applies only when using the agent's configured model).",
-    }),
-    temperature: Schema.optional(Schema.Finite),
-    top_p: Schema.optional(Schema.Finite),
-    prompt: Schema.optional(Schema.String),
-    tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
-      description: "@deprecated Use 'permission' field instead",
-    }),
-    disable: Schema.optional(Schema.Boolean),
-    description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
-    mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
-    hidden: Schema.optional(Schema.Boolean).annotate({
-      description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
-    }),
-    options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
-    color: Schema.optional(Color).annotate({
-      description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
-    }),
-    steps: Schema.optional(PositiveInt).annotate({
-      description: "Maximum number of agentic iterations before forcing text-only response",
-    }),
-    maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
-    permission: Schema.optional(ConfigPermission.Info),
-  }),
-  [Schema.Record(Schema.String, Schema.Any)],
-)
-
-const KNOWN_KEYS = new Set([
-  "name",
-  "model",
-  "variant",
-  "prompt",
-  "description",
-  "temperature",
-  "top_p",
-  "mode",
-  "hidden",
-  "color",
-  "steps",
-  "maxSteps",
-  "options",
-  "permission",
-  "disable",
-  "tools",
-])
-
-// Post-parse normalisation:
-//  - Promote any unknown-but-present keys into `options` so they survive the
-//    round-trip in a well-known field.
-//  - Translate the deprecated `tools: { name: boolean }` map into the new
-//    `permission` shape (write-adjacent tools collapse into `permission.edit`).
-//  - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias.
-const normalize = (agent: Schema.Schema.Type<typeof AgentSchema>): Schema.Schema.Type<typeof AgentSchema> => {
-  const options: Record<string, unknown> = { ...agent.options }
-  for (const [key, value] of Object.entries(agent)) {
-    if (!KNOWN_KEYS.has(key)) options[key] = value
-  }
-
-  const permission: ConfigPermission.Info = {}
-  for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
-    const action = enabled ? "allow" : "deny"
-    if (tool === "write" || tool === "edit" || tool === "patch") {
-      permission.edit = action
-      continue
-    }
-    permission[tool] = action
-  }
-  globalThis.Object.assign(permission, agent.permission)
-
-  const steps = agent.steps ?? agent.maxSteps
-  return { ...agent, options, permission, ...(steps !== undefined ? { steps } : {}) }
-}
-
-export const Info = AgentSchema.pipe(
-  Schema.decodeTo(AgentSchema, {
-    decode: SchemaGetter.transform(normalize),
-    encode: SchemaGetter.passthrough({ strict: false }),
-  }),
-).annotate({ identifier: "AgentConfig" })
-export type Info = Schema.Schema.Type<typeof Info>
-
 export async function load(dir: string) {
-  const result: Record<string, Info> = {}
+  const result: Record<string, ConfigAgentV1.Info> = {}
   for (const item of await Glob.scan("{agent,agents}/**/*.md", {
     cwd: dir,
     absolute: true,
@@ -124,13 +32,13 @@ export async function load(dir: string) {
       ...md.data,
       prompt: md.content.trim(),
     }
-    result[config.name] = ConfigParse.schema(Info, config, item)
+    result[config.name] = ConfigParse.schema(ConfigAgentV1.Info, config, item)
   }
   return result
 }
 
 export async function loadMode(dir: string) {
-  const result: Record<string, Info> = {}
+  const result: Record<string, ConfigAgentV1.Info> = {}
   for (const item of await Glob.scan("{mode,modes}/*.md", {
     cwd: dir,
     absolute: true,
@@ -148,7 +56,7 @@ export async function loadMode(dir: string) {
       ...md.data,
       prompt: md.content.trim(),
     }
-    const parsed = Schema.decodeUnknownExit(Info)(config, { errors: "all", propertyOrder: "original" })
+    const parsed = Schema.decodeUnknownExit(ConfigAgentV1.Info)(config, { errors: "all", propertyOrder: "original" })
     if (Exit.isSuccess(parsed)) {
       result[config.name] = {
         ...parsed.value,

+ 4 - 14
packages/opencode/src/config/command.ts

@@ -4,27 +4,17 @@ import path from "path"
 import * as Log from "@opencode-ai/core/util/log"
 import { Cause, Exit, Schema } from "effect"
 import { Glob } from "@opencode-ai/core/util/glob"
+import { ConfigCommandV1 } from "@opencode-ai/core/v1/config/command"
 import { configEntryNameFromPath } from "./entry-name"
-import { InvalidError } from "./error"
+import { InvalidError } from "@opencode-ai/core/v1/config/error"
 import * as ConfigMarkdown from "./markdown"
-import { ConfigModelID } from "./model-id"
 
 const log = Log.create({ service: "config" })
 
-export const Info = Schema.Struct({
-  template: Schema.String,
-  description: Schema.optional(Schema.String),
-  agent: Schema.optional(Schema.String),
-  model: Schema.optional(ConfigModelID),
-  subtask: Schema.optional(Schema.Boolean),
-})
-
-export type Info = Schema.Schema.Type<typeof Info>
-
-const decodeInfo = Schema.decodeUnknownExit(Info)
+const decodeInfo = Schema.decodeUnknownExit(ConfigCommandV1.Info)
 
 export async function load(dir: string) {
-  const result: Record<string, Info> = {}
+  const result: Record<string, ConfigCommandV1.Info> = {}
   for (const item of await Glob.scan("{command,commands}/**/*.md", {
     cwd: dir,
     absolute: true,

+ 13 - 221
packages/opencode/src/config/config.ts

@@ -6,7 +6,6 @@ import os from "os"
 import { mergeDeep } from "remeda"
 import { Global } from "@opencode-ai/core/global"
 import fsNode from "fs/promises"
-import { NamedError } from "@opencode-ai/core/util/error"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { Auth } from "../auth"
 import { Env } from "../env"
@@ -15,35 +14,25 @@ import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/instal
 import { existsSync } from "fs"
 import { Account } from "@/account/account"
 import { isRecord } from "@/util/record"
-import type { ConsoleState } from "./console-state"
+import type { ConsoleState } from "@opencode-ai/core/v1/config/console-state"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { InstanceState } from "@/effect/instance-state"
 import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
 import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
 import { containsPath, type InstanceContext } from "../project/instance-context"
-import { NonNegativeInt, PositiveInt, type DeepMutable } from "@opencode-ai/core/schema"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
+import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
+import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
 import { ConfigAgent } from "./agent"
-import { ConfigAttachment } from "./attachment"
 import { ConfigCommand } from "./command"
-import { ConfigFormatter } from "./formatter"
-import { ConfigLayout } from "./layout"
-import { ConfigLSP } from "./lsp"
 import { ConfigManaged } from "./managed"
-import { ConfigMCP } from "./mcp"
-import { ConfigModelID } from "./model-id"
 import { ConfigParse } from "./parse"
 import { ConfigPaths } from "./paths"
-import { ConfigPermission } from "./permission"
 import { ConfigPlugin } from "./plugin"
-import { ConfigProvider } from "./provider"
-import { ConfigReference } from "./reference"
-import { ConfigServer } from "./server"
-import { ConfigSkills } from "./skills"
 import { ConfigVariable } from "./variable"
 import { Npm } from "@opencode-ai/core/npm"
 import { withTransientReadRetry } from "@/util/effect-http-client"
-import { ConfigExperimental } from "@opencode-ai/core/config/experimental"
 
 const log = Log.create({ service: "config" })
 
@@ -110,12 +99,7 @@ async function substituteWellKnownRemoteConfig(input: {
   return { url, headers }
 }
 
-const WellKnownConfig = Schema.Struct({
-  config: Schema.optional(Schema.Json),
-  remote_config: Schema.optional(Schema.Json),
-})
-
-async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(config: T, filepath: string) {
+async function resolveLoadedPlugins<T extends { plugin?: ConfigPluginV1.Spec[] }>(config: T, filepath: string) {
   if (!config.plugin) return config
   for (let i = 0; i < config.plugin.length; i++) {
     // Normalize path-like plugin specs while we still know which config file declared them.
@@ -125,193 +109,7 @@ async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(
   return config
 }
 
-export type Layout = ConfigLayout.Layout
-
-const LogLevelRef = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
-  identifier: "LogLevel",
-  description: "Log level",
-})
-
-export const Info = Schema.Struct({
-  $schema: Schema.optional(Schema.String).annotate({
-    description: "JSON schema reference for configuration validation",
-  }),
-  shell: Schema.optional(Schema.String).annotate({
-    description: "Default shell to use for terminal and bash tool",
-  }),
-  logLevel: Schema.optional(LogLevelRef).annotate({ description: "Log level" }),
-  server: Schema.optional(ConfigServer.Server).annotate({
-    description: "Server configuration for opencode serve and web commands",
-  }),
-  command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
-    description: "Command configuration, see https://opencode.ai/docs/commands",
-  }),
-  skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
-  reference: Schema.optional(ConfigReference.Info).annotate({
-    description: "Named git or local directory references that can be mentioned as @alias or @alias/path",
-  }),
-  watcher: Schema.optional(
-    Schema.Struct({
-      ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
-    }),
-  ),
-  snapshot: Schema.optional(Schema.Boolean).annotate({
-    description:
-      "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
-  }),
-  // User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
-  plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPlugin.Spec))),
-  share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({
-    description:
-      "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
-  }),
-  autoshare: Schema.optional(Schema.Boolean).annotate({
-    description: "@deprecated Use 'share' field instead. Share newly created sessions automatically",
-  }),
-  autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({
-    description:
-      "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
-  }),
-  disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
-    description: "Disable providers that are loaded automatically",
-  }),
-  enabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
-    description: "When set, ONLY these providers will be enabled. All other providers will be ignored",
-  }),
-  model: Schema.optional(ConfigModelID).annotate({
-    description: "Model to use in the format of provider/model, eg anthropic/claude-2",
-  }),
-  small_model: Schema.optional(ConfigModelID).annotate({
-    description: "Small model to use for tasks like title generation in the format of provider/model",
-  }),
-  default_agent: Schema.optional(Schema.String).annotate({
-    description:
-      "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
-  }),
-  username: Schema.optional(Schema.String).annotate({
-    description: "Custom username to display in conversations instead of system username",
-  }),
-  mode: Schema.optional(
-    Schema.StructWithRest(
-      Schema.Struct({
-        build: Schema.optional(ConfigAgent.Info),
-        plan: Schema.optional(ConfigAgent.Info),
-      }),
-      [Schema.Record(Schema.String, ConfigAgent.Info)],
-    ),
-  ).annotate({ description: "@deprecated Use `agent` field instead." }),
-  agent: Schema.optional(
-    Schema.StructWithRest(
-      Schema.Struct({
-        // primary
-        plan: Schema.optional(ConfigAgent.Info),
-        build: Schema.optional(ConfigAgent.Info),
-        // subagent
-        general: Schema.optional(ConfigAgent.Info),
-        explore: Schema.optional(ConfigAgent.Info),
-        // specialized
-        title: Schema.optional(ConfigAgent.Info),
-        summary: Schema.optional(ConfigAgent.Info),
-        compaction: Schema.optional(ConfigAgent.Info),
-      }),
-      [Schema.Record(Schema.String, ConfigAgent.Info)],
-    ),
-  ).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }),
-  provider: Schema.optional(Schema.Record(Schema.String, ConfigProvider.Info)).annotate({
-    description: "Custom provider configurations and model overrides",
-  }),
-  mcp: Schema.optional(
-    Schema.Record(
-      Schema.String,
-      Schema.Union([
-        ConfigMCP.Info,
-        // Matches the legacy `{ enabled: false }` form used to disable a server.
-        Schema.Struct({ enabled: Schema.Boolean }),
-      ]),
-    ),
-  ).annotate({ description: "MCP (Model Context Protocol) server configurations" }),
-  formatter: Schema.optional(ConfigFormatter.Info).annotate({
-    description:
-      "Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides.",
-  }),
-  lsp: Schema.optional(ConfigLSP.Info).annotate({
-    description:
-      "Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides.",
-  }),
-  instructions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
-    description: "Additional instruction files or patterns to include",
-  }),
-  layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
-  permission: Schema.optional(ConfigPermission.Info),
-  tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
-  attachment: Schema.optional(ConfigAttachment.Info).annotate({
-    description: "Attachment processing configuration, including image size limits and resizing behavior",
-  }),
-  enterprise: Schema.optional(
-    Schema.Struct({
-      url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }),
-    }),
-  ),
-  tool_output: Schema.optional(
-    Schema.Struct({
-      max_lines: Schema.optional(PositiveInt).annotate({
-        description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)",
-      }),
-      max_bytes: Schema.optional(PositiveInt).annotate({
-        description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)",
-      }),
-    }),
-  ).annotate({
-    description:
-      "Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.",
-  }),
-  compaction: Schema.optional(
-    Schema.Struct({
-      auto: Schema.optional(Schema.Boolean).annotate({
-        description: "Enable automatic compaction when context is full (default: true)",
-      }),
-      prune: Schema.optional(Schema.Boolean).annotate({
-        description: "Enable pruning of old tool outputs (default: true)",
-      }),
-      tail_turns: Schema.optional(NonNegativeInt).annotate({
-        description:
-          "Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)",
-      }),
-      preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({
-        description: "Maximum number of tokens from recent turns to preserve verbatim after compaction",
-      }),
-      reserved: Schema.optional(NonNegativeInt).annotate({
-        description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
-      }),
-    }),
-  ),
-  experimental: Schema.optional(
-    Schema.Struct({
-      disable_paste_summary: Schema.optional(Schema.Boolean),
-      batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }),
-      openTelemetry: Schema.optional(Schema.Boolean).annotate({
-        description: "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)",
-      }),
-      primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
-        description: "Tools that should only be available to primary agents.",
-      }),
-      continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({
-        description: "Continue the agent loop when a tool call is denied",
-      }),
-      mcp_timeout: Schema.optional(PositiveInt).annotate({
-        description: "Timeout in milliseconds for model context protocol (MCP) requests",
-      }),
-      policies: Schema.optional(Schema.mutable(Schema.Array(ConfigExperimental.Policy))).annotate({
-        description: "Policy statements applied to supported resources, such as provider access",
-      }),
-    }),
-  ),
-}).annotate({ identifier: "Config" })
-
-// Uses the shared `DeepMutable` from `@opencode-ai/core/schema`. See the definition
-// there for why the local variant is needed over `Types.DeepMutable` from
-// effect-smol (the upstream version collapses `unknown` to `{}`).
-export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
+type Info = ConfigV1.Info & {
   // plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
   // with the file and scope it came from so later runtime code can make location-sensitive decisions.
   plugin_origins?: ConfigPlugin.Origin[]
@@ -375,12 +173,6 @@ function writableGlobal(info: Info) {
   return next
 }
 
-export const ConfigDirectoryTypoError = NamedError.create("ConfigDirectoryTypoError", {
-  path: Schema.String,
-  dir: Schema.String,
-  suggestion: Schema.String,
-})
-
 export const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
@@ -424,7 +216,7 @@ export const layer = Layer.effect(
         ),
       )
       const parsed = ConfigParse.jsonc(expanded, source)
-      const data = ConfigParse.schema(Info, normalizeLoadedConfig(parsed, source), source)
+      const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed, source), source)
       if (!("path" in options)) return data
 
       yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
@@ -530,7 +322,7 @@ export const layer = Layer.effect(
           source: string,
           // mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step
           // is attached.
-          list: ConfigPlugin.Spec[] | undefined,
+          list: ConfigPluginV1.Spec[] | undefined,
           // Scope can be inferred from the source path, but some callers already know whether the config should
           // behave as global or local and can pass that explicitly.
           kind?: ConfigPlugin.Scope,
@@ -558,7 +350,7 @@ export const layer = Layer.effect(
             authEnv[value.key] = value.token
             const wellknownURL = `${url}/.well-known/opencode`
             log.debug("fetching remote config", { url: wellknownURL })
-            const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, WellKnownConfig)
+            const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown)
             const remote = yield* Effect.promise(() =>
               substituteWellKnownRemoteConfig({
                 value: wellknown.remote_config,
@@ -753,9 +545,9 @@ export const layer = Layer.effect(
         }
 
         if (result.tools) {
-          const perms: Record<string, ConfigPermission.Action> = {}
+          const perms: Record<string, ConfigPermissionV1.Action> = {}
           for (const [tool, enabled] of Object.entries(result.tools)) {
-            const action: ConfigPermission.Action = enabled ? "allow" : "deny"
+            const action: ConfigPermissionV1.Action = enabled ? "allow" : "deny"
             if (tool === "write" || tool === "edit" || tool === "patch") {
               perms.edit = action
               continue
@@ -844,7 +636,7 @@ export const layer = Layer.effect(
       let next: Info
       let changed: boolean
       if (!file.endsWith(".jsonc")) {
-        const existing = ConfigParse.schema(Info, ConfigParse.jsonc(before, file), file)
+        const existing = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(before, file), file)
         const merged = mergeDeep(writable(existing), patch)
         const serialized = JSON.stringify(merged, null, 2)
         changed = serialized !== before
@@ -852,7 +644,7 @@ export const layer = Layer.effect(
         next = merged
       } else {
         const updated = patchJsonc(before, patch)
-        next = ConfigParse.schema(Info, ConfigParse.jsonc(updated, file), file)
+        next = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(updated, file), file)
         changed = updated !== before
         if (changed) yield* fs.writeFileString(file, updated).pipe(Effect.orDie)
       }

+ 1 - 7
packages/opencode/src/config/markdown.ts

@@ -1,7 +1,6 @@
-import { NamedError } from "@opencode-ai/core/util/error"
 import matter from "gray-matter"
-import { Schema } from "effect"
 import { Filesystem } from "@/util/filesystem"
+import { FrontmatterError } from "@opencode-ai/core/v1/config/error"
 
 export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
 export const SHELL_REGEX = /!`([^`]+)`/g
@@ -88,9 +87,4 @@ export async function parse(filePath: string) {
   }
 }
 
-export const FrontmatterError = NamedError.create("ConfigFrontmatterError", {
-  path: Schema.String,
-  message: Schema.String,
-})
-
 export * as ConfigMarkdown from "./markdown"

+ 0 - 5
packages/opencode/src/config/model-id.ts

@@ -1,5 +0,0 @@
-import { Schema } from "effect"
-
-export const ConfigModelID = Schema.String
-
-export type ConfigModelID = Schema.Schema.Type<typeof ConfigModelID>

+ 1 - 1
packages/opencode/src/config/parse.ts

@@ -3,7 +3,7 @@ export * as ConfigParse from "./parse"
 import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser"
 import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect"
 import type { DeepMutable } from "@opencode-ai/core/schema"
-import { InvalidError, JsonError } from "./error"
+import { InvalidError, JsonError } from "@opencode-ai/core/v1/config/error"
 
 export function jsonc(text: string, filepath: string): unknown {
   const errors: JsoncParseError[] = []

+ 6 - 14
packages/opencode/src/config/plugin.ts

@@ -1,30 +1,22 @@
 import { Glob } from "@opencode-ai/core/util/glob"
-import { Schema } from "effect"
+import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
 import { pathToFileURL } from "url"
 import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
 import path from "path"
 
-export const Options = Schema.Record(Schema.String, Schema.Unknown)
-export type Options = Schema.Schema.Type<typeof Options>
-
-// Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
-// It answers "what should we load?" but says nothing about where that value came from.
-export const Spec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, Options]))])
-export type Spec = Schema.Schema.Type<typeof Spec>
-
 export type Scope = "global" | "local"
 
 // Origin keeps the original config provenance attached to a spec.
 // After multiple config files are merged, callers still need to know which file declared the plugin
 // and whether it should behave like a global or project-local plugin.
 export type Origin = {
-  spec: Spec
+  spec: ConfigPluginV1.Spec
   source: string
   scope: Scope
 }
 
 export async function load(dir: string) {
-  const plugins: Spec[] = []
+  const plugins: ConfigPluginV1.Spec[] = []
 
   for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
     cwd: dir,
@@ -37,17 +29,17 @@ export async function load(dir: string) {
   return plugins
 }
 
-export function pluginSpecifier(plugin: Spec): string {
+export function pluginSpecifier(plugin: ConfigPluginV1.Spec): string {
   return Array.isArray(plugin) ? plugin[0] : plugin
 }
 
-export function pluginOptions(plugin: Spec): Options | undefined {
+export function pluginOptions(plugin: ConfigPluginV1.Spec): ConfigPluginV1.Options | undefined {
   return Array.isArray(plugin) ? plugin[1] : undefined
 }
 
 // Path-like specs are resolved relative to the config file that declared them so merges later on do not
 // accidentally reinterpret `./plugin.ts` relative to some other directory.
-export async function resolvePluginSpec(plugin: Spec, configFilepath: string): Promise<Spec> {
+export async function resolvePluginSpec(plugin: ConfigPluginV1.Spec, configFilepath: string): Promise<ConfigPluginV1.Spec> {
   const spec = pluginSpecifier(plugin)
   if (!isPathPluginSpec(spec)) return plugin
 

+ 3 - 24
packages/opencode/src/config/reference.ts

@@ -1,27 +1,6 @@
 export * as ConfigReference from "./reference"
 
-import { Schema } from "effect"
-
-const Git = Schema.Struct({
-  repository: Schema.String.annotate({
-    description: "Git repository URL, host/path reference, or GitHub owner/repo shorthand",
-  }),
-  branch: Schema.optional(Schema.String).annotate({
-    description: "Branch or ref to clone and inspect",
-  }),
-})
-
-const Local = Schema.Struct({
-  path: Schema.String.annotate({
-    description: "Absolute path, ~/ path, or workspace-relative path to a local reference directory",
-  }),
-})
-
-export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
-export type Entry = Schema.Schema.Type<typeof Entry>
-
-export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
-export type Info = Schema.Schema.Type<typeof Info>
+import { ConfigReferenceV1 } from "@opencode-ai/core/v1/config/reference"
 
 export type NormalizedEntry =
   | {
@@ -47,7 +26,7 @@ export function validateAlias(name: string) {
   }
 }
 
-export function normalizeEntry(entry: Entry): NormalizedEntry {
+export function normalizeEntry(entry: ConfigReferenceV1.Entry): NormalizedEntry {
   if (typeof entry === "string") {
     if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) {
       return { kind: "local", path: entry }
@@ -59,7 +38,7 @@ export function normalizeEntry(entry: Entry): NormalizedEntry {
   return { kind: "git", repository: entry.repository, branch: entry.branch }
 }
 
-export function normalize(info: Info): NormalizedInfo {
+export function normalize(info: ConfigReferenceV1.Info): NormalizedInfo {
   return Object.fromEntries(
     Object.entries(info).map(([name, entry]) => {
       const aliasError = validateAlias(name)

+ 1 - 1
packages/opencode/src/config/variable.ts

@@ -3,7 +3,7 @@ export * as ConfigVariable from "./variable"
 import path from "path"
 import os from "os"
 import { Filesystem } from "@/util/filesystem"
-import { InvalidError } from "./error"
+import { InvalidError } from "@opencode-ai/core/v1/config/error"
 
 type ParseSource =
   | {

+ 3 - 3
packages/opencode/src/image/image.ts

@@ -1,5 +1,5 @@
 import { Config } from "@/config/config"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import type { MessageV2 } from "@/session/message-v2"
 import * as Log from "@opencode-ai/core/util/log"
 import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
@@ -53,7 +53,7 @@ export class SizeError extends Schema.TaggedErrorClass<SizeError>()("ImageSizeEr
 export type Error = ResizerUnavailableError | InvalidDataUrlError | DecodeError | SizeError
 
 export interface Interface {
-  readonly normalize: (input: SessionLegacy.FilePart) => Effect.Effect<SessionLegacy.FilePart, Error>
+  readonly normalize: (input: SessionV1.FilePart) => Effect.Effect<SessionV1.FilePart, Error>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/Image") {}
@@ -74,7 +74,7 @@ export const layer = Layer.effect(
       ),
     )
 
-    const normalize = Effect.fn("Image.normalize")(function* (input: SessionLegacy.FilePart) {
+    const normalize = Effect.fn("Image.normalize")(function* (input: SessionV1.FilePart) {
       const image = (yield* config.get()).attachment?.image
       const info = {
         autoResize: image?.auto_resize ?? AUTO_RESIZE,

+ 13 - 12
packages/opencode/src/mcp/index.ts

@@ -1,4 +1,5 @@
 import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { serviceUse } from "@opencode-ai/core/effect/service-use"
 import { Client } from "@modelcontextprotocol/sdk/client/index.js"
 import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
@@ -13,7 +14,7 @@ import {
   ToolListChangedNotificationSchema,
 } from "@modelcontextprotocol/sdk/types.js"
 import { Config } from "@/config/config"
-import { ConfigMCP } from "../config/mcp"
+import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
 import * as Log from "@opencode-ai/core/util/log"
 import { NamedError } from "@opencode-ai/core/util/error"
 import { InstallationVersion } from "@opencode-ai/core/installation/version"
@@ -106,9 +107,9 @@ const pendingOAuthTransports = new Map<string, TransportWithAuth>()
 // Prompt cache types
 type PromptInfo = Awaited<ReturnType<MCPClient["listPrompts"]>>["prompts"][number]
 type ResourceInfo = Awaited<ReturnType<MCPClient["listResources"]>>["resources"][number]
-type McpEntry = NonNullable<Config.Info["mcp"]>[string]
+type McpEntry = NonNullable<ConfigV1.Info["mcp"]>[string]
 
-function isMcpConfigured(entry: McpEntry): entry is ConfigMCP.Info {
+function isMcpConfigured(entry: McpEntry): entry is ConfigMCPV1.Info {
   return typeof entry === "object" && entry !== null && "type" in entry
 }
 
@@ -234,7 +235,7 @@ interface AuthResult {
 // --- Effect Service ---
 
 interface State {
-  config: Record<string, ConfigMCP.Info>
+  config: Record<string, ConfigMCPV1.Info>
   status: Record<string, Status>
   clients: Record<string, MCPClient>
   defs: Record<string, MCPToolDef[]>
@@ -246,7 +247,7 @@ export interface Interface {
   readonly tools: () => Effect.Effect<Record<string, Tool>>
   readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
   readonly resources: () => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
-  readonly add: (name: string, mcp: ConfigMCP.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
+  readonly add: (name: string, mcp: ConfigMCPV1.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
   readonly connect: (name: string) => Effect.Effect<void, NotFoundError>
   readonly disconnect: (name: string) => Effect.Effect<void, NotFoundError>
   readonly getPrompt: (
@@ -304,7 +305,7 @@ export const layer = Layer.effect(
 
     const connectRemote = Effect.fn("MCP.connectRemote")(function* (
       key: string,
-      mcp: ConfigMCP.Info & { type: "remote" },
+      mcp: ConfigMCPV1.Info & { type: "remote" },
     ) {
       const oauthDisabled = mcp.oauth === false
       const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined
@@ -421,7 +422,7 @@ export const layer = Layer.effect(
 
     const connectLocal = Effect.fn("MCP.connectLocal")(function* (
       key: string,
-      mcp: ConfigMCP.Info & { type: "local" },
+      mcp: ConfigMCPV1.Info & { type: "local" },
     ) {
       const [cmd, ...args] = mcp.command
       const cwd = yield* InstanceState.directory
@@ -454,7 +455,7 @@ export const layer = Layer.effect(
       )
     })
 
-    const create = Effect.fn("MCP.create")(function* (key: string, mcp: ConfigMCP.Info) {
+    const create = Effect.fn("MCP.create")(function* (key: string, mcp: ConfigMCPV1.Info) {
       if (mcp.enabled === false) {
         log.info("mcp server disabled", { key })
         return DISABLED_RESULT
@@ -464,8 +465,8 @@ export const layer = Layer.effect(
 
       const { client: mcpClient, status } =
         mcp.type === "remote"
-          ? yield* connectRemote(key, mcp as ConfigMCP.Info & { type: "remote" })
-          : yield* connectLocal(key, mcp as ConfigMCP.Info & { type: "local" })
+          ? yield* connectRemote(key, mcp as ConfigMCPV1.Info & { type: "remote" })
+          : yield* connectLocal(key, mcp as ConfigMCPV1.Info & { type: "local" })
 
       if (!mcpClient) {
         return { status } satisfies CreateResult
@@ -633,7 +634,7 @@ export const layer = Layer.effect(
       return s.clients
     })
 
-    const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCP.Info) {
+    const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCPV1.Info) {
       const s = yield* InstanceState.get(state)
       const result = yield* create(name, mcp)
 
@@ -647,7 +648,7 @@ export const layer = Layer.effect(
       return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout)
     })
 
-    const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCP.Info) {
+    const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCPV1.Info) {
       const s = yield* InstanceState.get(state)
       s.config[name] = mcp
       yield* createAndStore(name, mcp)

+ 31 - 31
packages/opencode/src/permission/index.ts

@@ -1,48 +1,48 @@
-import { ConfigPermission } from "@/config/permission"
+import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
 import { InstanceState } from "@/effect/instance-state"
 import * as Log from "@opencode-ai/core/util/log"
 import { Wildcard } from "@opencode-ai/core/util/wildcard"
 import { Deferred, Effect, Layer, Context } from "effect"
 import os from "os"
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { EventV2Bridge } from "@/event-v2-bridge"
 import { EventV2 } from "@opencode-ai/core/event"
 
 const log = Log.create({ service: "permission" })
 
 export const Event = {
-  Asked: EventV2.define({ type: "permission.asked", schema: PermissionLegacy.Request.fields }),
+  Asked: EventV2.define({ type: "permission.asked", schema: PermissionV1.Request.fields }),
   Replied: EventV2.define({
     type: "permission.replied",
     schema: {
-      sessionID: PermissionLegacy.Request.fields.sessionID,
-      requestID: PermissionLegacy.ID,
-      reply: PermissionLegacy.Reply,
+      sessionID: PermissionV1.Request.fields.sessionID,
+      requestID: PermissionV1.ID,
+      reply: PermissionV1.Reply,
     },
   }),
 }
 
 export interface Interface {
-  readonly ask: (input: PermissionLegacy.AskInput) => Effect.Effect<void, PermissionLegacy.Error>
-  readonly reply: (input: PermissionLegacy.ReplyInput) => Effect.Effect<void, PermissionLegacy.NotFoundError>
-  readonly list: () => Effect.Effect<ReadonlyArray<PermissionLegacy.Request>>
+  readonly ask: (input: PermissionV1.AskInput) => Effect.Effect<void, PermissionV1.Error>
+  readonly reply: (input: PermissionV1.ReplyInput) => Effect.Effect<void, PermissionV1.NotFoundError>
+  readonly list: () => Effect.Effect<ReadonlyArray<PermissionV1.Request>>
 }
 
 interface PendingEntry {
-  info: PermissionLegacy.Request
-  deferred: Deferred.Deferred<void, PermissionLegacy.RejectedError | PermissionLegacy.CorrectedError>
+  info: PermissionV1.Request
+  deferred: Deferred.Deferred<void, PermissionV1.RejectedError | PermissionV1.CorrectedError>
 }
 
 interface State {
-  pending: Map<PermissionLegacy.ID, PendingEntry>
-  approved: PermissionLegacy.Rule[]
+  pending: Map<PermissionV1.ID, PendingEntry>
+  approved: PermissionV1.Rule[]
 }
 
 export function evaluate(
   permission: string,
   pattern: string,
-  ...rulesets: PermissionLegacy.Ruleset[]
-): PermissionLegacy.Rule {
+  ...rulesets: PermissionV1.Ruleset[]
+): PermissionV1.Rule {
   return (
     rulesets
       .flat()
@@ -64,14 +64,14 @@ export const layer = Layer.effect(
       Effect.fn("Permission.state")(function* (ctx) {
         void ctx
         const state = {
-          pending: new Map<PermissionLegacy.ID, PendingEntry>(),
+          pending: new Map<PermissionV1.ID, PendingEntry>(),
           approved: [],
         }
 
         yield* Effect.addFinalizer(() =>
           Effect.gen(function* () {
             for (const item of state.pending.values()) {
-              yield* Deferred.fail(item.deferred, new PermissionLegacy.RejectedError())
+              yield* Deferred.fail(item.deferred, new PermissionV1.RejectedError())
             }
             state.pending.clear()
           }),
@@ -81,7 +81,7 @@ export const layer = Layer.effect(
       }),
     )
 
-    const ask = Effect.fn("Permission.ask")(function* (input: PermissionLegacy.AskInput) {
+    const ask = Effect.fn("Permission.ask")(function* (input: PermissionV1.AskInput) {
       const { approved, pending } = yield* InstanceState.get(state)
       const { ruleset, ...request } = input
       let needsAsk = false
@@ -90,7 +90,7 @@ export const layer = Layer.effect(
         const rule = evaluate(request.permission, pattern, ruleset, approved)
         log.info("evaluated", { permission: request.permission, pattern, action: rule })
         if (rule.action === "deny") {
-          return yield* new PermissionLegacy.DeniedError({
+          return yield* new PermissionV1.DeniedError({
             ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)),
           })
         }
@@ -100,8 +100,8 @@ export const layer = Layer.effect(
 
       if (!needsAsk) return
 
-      const id = request.id ?? PermissionLegacy.ID.ascending()
-      const info: PermissionLegacy.Request = {
+      const id = request.id ?? PermissionV1.ID.ascending()
+      const info: PermissionV1.Request = {
         id,
         sessionID: request.sessionID,
         permission: request.permission,
@@ -112,7 +112,7 @@ export const layer = Layer.effect(
       }
       log.info("asking", { id, permission: info.permission, patterns: info.patterns })
 
-      const deferred = yield* Deferred.make<void, PermissionLegacy.RejectedError | PermissionLegacy.CorrectedError>()
+      const deferred = yield* Deferred.make<void, PermissionV1.RejectedError | PermissionV1.CorrectedError>()
       pending.set(id, { info, deferred })
       yield* events.publish(Event.Asked, info)
       return yield* Effect.ensuring(
@@ -123,10 +123,10 @@ export const layer = Layer.effect(
       )
     })
 
-    const reply = Effect.fn("Permission.reply")(function* (input: PermissionLegacy.ReplyInput) {
+    const reply = Effect.fn("Permission.reply")(function* (input: PermissionV1.ReplyInput) {
       const { approved, pending } = yield* InstanceState.get(state)
       const existing = pending.get(input.requestID)
-      if (!existing) return yield* new PermissionLegacy.NotFoundError({ requestID: input.requestID })
+      if (!existing) return yield* new PermissionV1.NotFoundError({ requestID: input.requestID })
 
       pending.delete(input.requestID)
       yield* events.publish(Event.Replied, {
@@ -139,8 +139,8 @@ export const layer = Layer.effect(
         yield* Deferred.fail(
           existing.deferred,
           input.message
-            ? new PermissionLegacy.CorrectedError({ feedback: input.message })
-            : new PermissionLegacy.RejectedError(),
+            ? new PermissionV1.CorrectedError({ feedback: input.message })
+            : new PermissionV1.RejectedError(),
         )
 
         for (const [id, item] of pending.entries()) {
@@ -151,7 +151,7 @@ export const layer = Layer.effect(
             requestID: item.info.id,
             reply: "reject",
           })
-          yield* Deferred.fail(item.deferred, new PermissionLegacy.RejectedError())
+          yield* Deferred.fail(item.deferred, new PermissionV1.RejectedError())
         }
         return
       }
@@ -200,8 +200,8 @@ function expand(pattern: string): string {
   return pattern
 }
 
-export function fromConfig(permission: ConfigPermission.Info) {
-  const ruleset: PermissionLegacy.Rule[] = []
+export function fromConfig(permission: ConfigPermissionV1.Info) {
+  const ruleset: PermissionV1.Rule[] = []
   for (const [key, value] of Object.entries(permission)) {
     if (typeof value === "string") {
       ruleset.push({ permission: key, action: value, pattern: "*" })
@@ -214,11 +214,11 @@ export function fromConfig(permission: ConfigPermission.Info) {
   return ruleset
 }
 
-export function merge(...rulesets: PermissionLegacy.Ruleset[]): PermissionLegacy.Rule[] {
+export function merge(...rulesets: PermissionV1.Ruleset[]): PermissionV1.Rule[] {
   return rulesets.flat()
 }
 
-export function disabled(tools: string[], ruleset: PermissionLegacy.Ruleset): Set<string> {
+export function disabled(tools: string[], ruleset: PermissionV1.Ruleset): Set<string> {
   const edits = ["edit", "write", "apply_patch"]
   return new Set(
     tools.filter((tool) => {

+ 3 - 2
packages/opencode/src/plugin/loader.ts

@@ -9,13 +9,14 @@ import {
   type PluginSource,
 } from "./shared"
 import { ConfigPlugin } from "@/config/plugin"
+import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
 import { InstallationVersion } from "@opencode-ai/core/installation/version"
 
 export namespace PluginLoader {
   // A normalized plugin declaration derived from config before any filesystem or npm work happens.
   export type Plan = {
     spec: string
-    options: ConfigPlugin.Options | undefined
+    options: ConfigPluginV1.Options | undefined
     deprecated: boolean
   }
 
@@ -73,7 +74,7 @@ export namespace PluginLoader {
   }
 
   // Normalize a config item into the loader's internal representation.
-  function plan(item: ConfigPlugin.Spec): Plan {
+  function plan(item: ConfigPluginV1.Spec): Plan {
     const spec = ConfigPlugin.pluginSpecifier(item)
     return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
   }

+ 2 - 1
packages/opencode/src/provider/provider.ts

@@ -1,4 +1,5 @@
 import os from "os"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import fuzzysort from "fuzzysort"
 import { Config } from "@/config/config"
 import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
@@ -142,7 +143,7 @@ type CustomLoader = (provider: Info) => Effect.Effect<{
 
 type CustomDep = {
   auth: (id: string) => Effect.Effect<Auth.Info | undefined>
-  config: () => Effect.Effect<Config.Info>
+  config: () => Effect.Effect<ConfigV1.Info>
   env: () => Effect.Effect<Record<string, string | undefined>>
   get: (key: string) => Effect.Effect<string | undefined>
 }

+ 4 - 3
packages/opencode/src/server/routes/instance/httpapi/groups/config.ts

@@ -1,4 +1,5 @@
 import { Config } from "@/config/config"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { Provider } from "@/provider/provider"
 import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 import { Authorization } from "../middleware/authorization"
@@ -14,7 +15,7 @@ export const ConfigApi = HttpApi.make("config")
       .add(
         HttpApiEndpoint.get("get", root, {
           query: WorkspaceRoutingQuery,
-          success: described(Config.Info, "Get config info"),
+          success: described(ConfigV1.Info, "Get config info"),
         }).annotateMerge(
           OpenApi.annotations({
             identifier: "config.get",
@@ -24,8 +25,8 @@ export const ConfigApi = HttpApi.make("config")
         ),
         HttpApiEndpoint.patch("update", root, {
           query: WorkspaceRoutingQuery,
-          payload: Config.Info,
-          success: described(Config.Info, "Successfully updated config"),
+          payload: ConfigV1.Info,
+          success: described(ConfigV1.Info, "Successfully updated config"),
           error: HttpApiError.BadRequest,
         }).annotateMerge(
           OpenApi.annotations({

+ 4 - 3
packages/opencode/src/server/routes/instance/httpapi/groups/global.ts

@@ -1,4 +1,5 @@
 import { Config } from "@/config/config"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { EventV2 } from "@opencode-ai/core/event"
 import { InstanceDisposed } from "@/server/event"
 import "@opencode-ai/core/account"
@@ -90,7 +91,7 @@ export const GlobalApi = HttpApi.make("global").add(
         }),
       ),
       HttpApiEndpoint.get("configGet", GlobalPaths.config, {
-        success: described(Config.Info, "Get global config info"),
+        success: described(ConfigV1.Info, "Get global config info"),
       }).annotateMerge(
         OpenApi.annotations({
           identifier: "global.config.get",
@@ -99,8 +100,8 @@ export const GlobalApi = HttpApi.make("global").add(
         }),
       ),
       HttpApiEndpoint.patch("configUpdate", GlobalPaths.config, {
-        payload: Config.Info,
-        success: described(Config.Info, "Successfully updated global config"),
+        payload: ConfigV1.Info,
+        success: described(ConfigV1.Info, "Successfully updated global config"),
         error: HttpApiError.BadRequest,
       }).annotateMerge(
         OpenApi.annotations({

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

@@ -1,5 +1,5 @@
 import { MCP } from "@/mcp"
-import { ConfigMCP } from "@/config/mcp"
+import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
 import { Schema } from "effect"
 import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 import { McpServerNotFoundError } from "../errors"
@@ -10,7 +10,7 @@ import { described } from "./metadata"
 
 export const AddPayload = Schema.Struct({
   name: Schema.String,
-  config: ConfigMCP.Info,
+  config: ConfigMCPV1.Info,
 })
 
 export const StatusMap = Schema.Record(Schema.String, MCP.Status)

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

@@ -1,4 +1,4 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Permission } from "@/permission"
 import { Schema } from "effect"
 import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -10,7 +10,7 @@ import { described } from "./metadata"
 
 const root = "/permission"
 const ReplyPayload = Schema.Struct({
-  reply: PermissionLegacy.Reply,
+  reply: PermissionV1.Reply,
   message: Schema.optional(Schema.String),
 })
 
@@ -20,7 +20,7 @@ export const PermissionApi = HttpApi.make("permission")
       .add(
         HttpApiEndpoint.get("list", root, {
           query: WorkspaceRoutingQuery,
-          success: described(Schema.Array(PermissionLegacy.Request), "List of pending permissions"),
+          success: described(Schema.Array(PermissionV1.Request), "List of pending permissions"),
         }).annotateMerge(
           OpenApi.annotations({
             identifier: "permission.list",
@@ -29,7 +29,7 @@ export const PermissionApi = HttpApi.make("permission")
           }),
         ),
         HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
-          params: { requestID: PermissionLegacy.ID },
+          params: { requestID: PermissionV1.ID },
           query: WorkspaceRoutingQuery,
           payload: ReplyPayload,
           success: described(Schema.Boolean, "Permission processed successfully"),

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

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Permission } from "@/permission"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 
 import { Session } from "@/session/session"
 import { MessageV2 } from "@/session/message-v2"
@@ -48,7 +48,7 @@ export const StatusMap = Schema.Record(Schema.String, SessionStatus.Info)
 export const UpdatePayload = Schema.Struct({
   title: Schema.optional(Schema.String),
   metadata: Schema.optional(Session.Metadata),
-  permission: Schema.optional(PermissionLegacy.Ruleset),
+  permission: Schema.optional(PermissionV1.Ruleset),
   time: Schema.optional(
     Schema.Struct({
       archived: Schema.optional(Session.ArchivedTimestamp),
@@ -71,7 +71,7 @@ export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInp
 export const ShellPayload = Schema.Struct(Struct.omit(SessionPrompt.ShellInput.fields, ["sessionID"]))
 export const RevertPayload = Schema.Struct(Struct.omit(SessionRevert.RevertInput.fields, ["sessionID"]))
 export const PermissionResponsePayload = Schema.Struct({
-  response: PermissionLegacy.Reply,
+  response: PermissionV1.Reply,
 })
 
 export const SessionPaths = {
@@ -178,7 +178,7 @@ export const SessionApi = HttpApi.make("session")
         HttpApiEndpoint.get("messages", SessionPaths.messages, {
           params: { sessionID: SessionID },
           query: MessagesQuery,
-          success: described(Schema.Array(SessionLegacy.WithParts), "List of messages"),
+          success: described(Schema.Array(SessionV1.WithParts), "List of messages"),
           error: [HttpApiError.BadRequest, ApiNotFoundError],
         }).annotateMerge(
           OpenApi.annotations({
@@ -190,7 +190,7 @@ export const SessionApi = HttpApi.make("session")
         HttpApiEndpoint.get("message", SessionPaths.message, {
           params: { sessionID: SessionID, messageID: MessageID },
           query: WorkspaceRoutingQuery,
-          success: described(SessionLegacy.WithParts, "Message"),
+          success: described(SessionV1.WithParts, "Message"),
           error: [HttpApiError.BadRequest, ApiNotFoundError],
         }).annotateMerge(
           OpenApi.annotations({
@@ -316,7 +316,7 @@ export const SessionApi = HttpApi.make("session")
           params: { sessionID: SessionID },
           query: WorkspaceRoutingQuery,
           payload: PromptPayload,
-          success: described(SessionLegacy.WithParts, "Created message"),
+          success: described(SessionV1.WithParts, "Created message"),
           error: [HttpApiError.BadRequest, ApiNotFoundError],
         }).annotateMerge(
           OpenApi.annotations({
@@ -343,7 +343,7 @@ export const SessionApi = HttpApi.make("session")
           params: { sessionID: SessionID },
           query: WorkspaceRoutingQuery,
           payload: CommandPayload,
-          success: described(SessionLegacy.WithParts, "Created message"),
+          success: described(SessionV1.WithParts, "Created message"),
           error: [HttpApiError.BadRequest, ApiNotFoundError],
         }).annotateMerge(
           OpenApi.annotations({
@@ -356,7 +356,7 @@ export const SessionApi = HttpApi.make("session")
           params: { sessionID: SessionID },
           query: WorkspaceRoutingQuery,
           payload: ShellPayload,
-          success: described(SessionLegacy.WithParts, "Created message"),
+          success: described(SessionV1.WithParts, "Created message"),
           error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
         }).annotateMerge(
           OpenApi.annotations({
@@ -392,7 +392,7 @@ export const SessionApi = HttpApi.make("session")
           }),
         ),
         HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
-          params: { sessionID: SessionID, permissionID: PermissionLegacy.ID },
+          params: { sessionID: SessionID, permissionID: PermissionV1.ID },
           query: WorkspaceRoutingQuery,
           payload: PermissionResponsePayload,
           success: described(Schema.Boolean, "Permission processed successfully"),
@@ -432,8 +432,8 @@ export const SessionApi = HttpApi.make("session")
         HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
           params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
           query: WorkspaceRoutingQuery,
-          payload: SessionLegacy.Part,
-          success: described(SessionLegacy.Part, "Successfully updated part"),
+          payload: SessionV1.Part,
+          success: described(SessionV1.Part, "Successfully updated part"),
           error: [HttpApiError.BadRequest, ApiNotFoundError],
         }).annotateMerge(
           OpenApi.annotations({

+ 3 - 3
packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts

@@ -1,4 +1,4 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Permission } from "@/permission"
 import { Effect } from "effect"
 import { HttpApiBuilder } from "effect/unstable/httpapi"
@@ -14,8 +14,8 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permiss
     })
 
     const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: {
-      params: { requestID: PermissionLegacy.ID }
-      payload: PermissionLegacy.ReplyBody
+      params: { requestID: PermissionV1.ID }
+      payload: PermissionV1.ReplyBody
     }) {
       yield* svc
         .reply({

+ 5 - 5
packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Agent } from "@/agent/agent"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { EventV2Bridge } from "@/event-v2-bridge"
 import { Command } from "@/command"
 import { Permission } from "@/permission"
@@ -360,7 +360,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
     })
 
     const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
-      params: { sessionID: SessionID; permissionID: PermissionLegacy.ID }
+      params: { sessionID: SessionID; permissionID: PermissionV1.ID }
       payload: typeof PermissionResponsePayload.Type
     }) {
       yield* requireSession(ctx.params.sessionID)
@@ -396,10 +396,10 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
 
     const updatePart = Effect.fn("SessionHttpApi.updatePart")(function* (ctx: {
       params: { sessionID: SessionID; messageID: MessageID; partID: PartID }
-      payload: typeof SessionLegacy.Part.Type
+      payload: typeof SessionV1.Part.Type
     }) {
       yield* requireSession(ctx.params.sessionID)
-      const payload = ctx.payload as SessionLegacy.Part
+      const payload = ctx.payload as SessionV1.Part
       if (
         payload.id !== ctx.params.partID ||
         payload.messageID !== ctx.params.messageID ||

+ 22 - 21
packages/opencode/src/session/compaction.ts

@@ -1,4 +1,5 @@
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { Session } from "./session"
 import { SessionID, MessageID, PartID } from "./schema"
 import { Provider } from "@/provider/provider"
@@ -93,9 +94,9 @@ type CompletedCompaction = {
   summary: string | undefined
 }
 
-function summaryText(message: SessionLegacy.WithParts) {
+function summaryText(message: SessionV1.WithParts) {
   const text = message.parts
-    .filter((part): part is SessionLegacy.TextPart => part.type === "text")
+    .filter((part): part is SessionV1.TextPart => part.type === "text")
     .map((part) => part.text.trim())
     .filter(Boolean)
     .join("\n\n")
@@ -103,7 +104,7 @@ function summaryText(message: SessionLegacy.WithParts) {
   return text || undefined
 }
 
-function completedCompactions(messages: SessionLegacy.WithParts[]) {
+function completedCompactions(messages: SessionV1.WithParts[]) {
   const users = new Map<MessageID, number>()
   for (let i = 0; i < messages.length; i++) {
     const msg = messages[i]
@@ -134,14 +135,14 @@ function buildPrompt(input: { previousSummary?: string; context: string[] }) {
   return [anchor, SUMMARY_TEMPLATE, ...input.context].join("\n\n")
 }
 
-function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) {
+function preserveRecentBudget(input: { cfg: ConfigV1.Info; model: Provider.Model }) {
   return (
     input.cfg.compaction?.preserve_recent_tokens ??
     Math.min(MAX_PRESERVE_RECENT_TOKENS, Math.max(MIN_PRESERVE_RECENT_TOKENS, Math.floor(usable(input) * 0.25)))
   )
 }
 
-function turns(messages: SessionLegacy.WithParts[]) {
+function turns(messages: SessionV1.WithParts[]) {
   const result: Turn[] = []
   for (let i = 0; i < messages.length; i++) {
     const msg = messages[i]
@@ -160,11 +161,11 @@ function turns(messages: SessionLegacy.WithParts[]) {
 }
 
 function splitTurn(input: {
-  messages: SessionLegacy.WithParts[]
+  messages: SessionV1.WithParts[]
   turn: Turn
   model: Provider.Model
   budget: number
-  estimate: (input: { messages: SessionLegacy.WithParts[]; model: Provider.Model }) => Effect.Effect<number>
+  estimate: (input: { messages: SessionV1.WithParts[]; model: Provider.Model }) => Effect.Effect<number>
 }) {
   return Effect.gen(function* () {
     if (input.budget <= 0) return undefined
@@ -186,13 +187,13 @@ function splitTurn(input: {
 
 export interface Interface {
   readonly isOverflow: (input: {
-    tokens: SessionLegacy.Assistant["tokens"]
+    tokens: SessionV1.Assistant["tokens"]
     model: Provider.Model
   }) => Effect.Effect<boolean>
   readonly prune: (input: { sessionID: SessionID }) => Effect.Effect<void>
   readonly process: (input: {
     parentID: MessageID
-    messages: SessionLegacy.WithParts[]
+    messages: SessionV1.WithParts[]
     sessionID: SessionID
     auto: boolean
     overflow?: boolean
@@ -223,7 +224,7 @@ export const layer = Layer.effect(
     const flags = yield* RuntimeFlags.Service
 
     const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: {
-      tokens: SessionLegacy.Assistant["tokens"]
+      tokens: SessionV1.Assistant["tokens"]
       model: Provider.Model
     }) {
       return overflow({
@@ -235,7 +236,7 @@ export const layer = Layer.effect(
     })
 
     const estimate = Effect.fn("SessionCompaction.estimate")(function* (input: {
-      messages: SessionLegacy.WithParts[]
+      messages: SessionV1.WithParts[]
       model: Provider.Model
     }) {
       const msgs = yield* MessageV2.toModelMessagesEffect(input.messages, input.model)
@@ -243,8 +244,8 @@ export const layer = Layer.effect(
     })
 
     const select = Effect.fn("SessionCompaction.select")(function* (input: {
-      messages: SessionLegacy.WithParts[]
-      cfg: Config.Info
+      messages: SessionV1.WithParts[]
+      cfg: ConfigV1.Info
       model: Provider.Model
     }) {
       const limit = input.cfg.compaction?.tail_turns ?? DEFAULT_TAIL_TURNS
@@ -307,7 +308,7 @@ export const layer = Layer.effect(
 
       let total = 0
       let pruned = 0
-      const toPrune: SessionLegacy.ToolPart[] = []
+      const toPrune: SessionV1.ToolPart[] = []
       let turns = 0
 
       loop: for (let msgIndex = msgs.length - 1; msgIndex >= 0; msgIndex--) {
@@ -343,7 +344,7 @@ export const layer = Layer.effect(
 
     const processCompaction = Effect.fn("SessionCompaction.process")(function* (input: {
       parentID: MessageID
-      messages: SessionLegacy.WithParts[]
+      messages: SessionV1.WithParts[]
       sessionID: SessionID
       auto: boolean
       overflow?: boolean
@@ -354,14 +355,14 @@ export const layer = Layer.effect(
       }
       const userMessage = parent.info
       const compactionPart = parent.parts.find(
-        (part): part is SessionLegacy.CompactionPart => part.type === "compaction",
+        (part): part is SessionV1.CompactionPart => part.type === "compaction",
       )
 
       let messages = input.messages
       let replay:
         | {
-            info: SessionLegacy.User
-            parts: SessionLegacy.Part[]
+            info: SessionV1.User
+            parts: SessionV1.Part[]
           }
         | undefined
       if (input.overflow) {
@@ -410,7 +411,7 @@ export const layer = Layer.effect(
         toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
       })
       const ctx = yield* InstanceState.context
-      const msg: SessionLegacy.Assistant = {
+      const msg: SessionV1.Assistant = {
         id: MessageID.ascending(),
         role: "assistant",
         parentID: input.parentID,
@@ -459,7 +460,7 @@ export const layer = Layer.effect(
       })
 
       if (result === "compact") {
-        processor.message.error = new SessionLegacy.ContextOverflowError({
+        processor.message.error = new SessionV1.ContextOverflowError({
           message: replay
             ? "Conversation history too large to compact - exceeds model context limit"
             : "Session too large to compact - context exceeds model limit even after stripping media",

+ 5 - 5
packages/opencode/src/session/instruction.ts

@@ -1,5 +1,5 @@
 import path from "path"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Effect, Layer, Context } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
 import { Config } from "@/config/config"
@@ -12,7 +12,7 @@ import { Global } from "@opencode-ai/core/global"
 import type { MessageV2 } from "./message-v2"
 import type { MessageID } from "./schema"
 
-function extract(messages: SessionLegacy.WithParts[]) {
+function extract(messages: SessionV1.WithParts[]) {
   const paths = new Set<string>()
   for (const msg of messages) {
     for (const part of msg.parts) {
@@ -35,7 +35,7 @@ export interface Interface {
   readonly system: () => Effect.Effect<string[], FSUtil.Error>
   readonly find: (dir: string) => Effect.Effect<string | undefined, FSUtil.Error>
   readonly resolve: (
-    messages: SessionLegacy.WithParts[],
+    messages: SessionV1.WithParts[],
     filepath: string,
     messageID: MessageID,
   ) => Effect.Effect<{ filepath: string; content: string }[], FSUtil.Error>
@@ -175,7 +175,7 @@ export const layer: Layer.Layer<
     })
 
     const resolve = Effect.fn("Instruction.resolve")(function* (
-      messages: SessionLegacy.WithParts[],
+      messages: SessionV1.WithParts[],
       filepath: string,
       messageID: MessageID,
     ) {
@@ -230,7 +230,7 @@ export const defaultLayer = layer.pipe(
   Layer.provide(RuntimeFlags.defaultLayer),
 )
 
-export function loaded(messages: SessionLegacy.WithParts[]) {
+export function loaded(messages: SessionV1.WithParts[]) {
   return extract(messages)
 }
 

+ 5 - 5
packages/opencode/src/session/llm.ts

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Provider } from "@/provider/provider"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { serviceUse } from "@opencode-ai/core/effect/service-use"
 import { Log } from "@opencode-ai/core/util/log"
 import { Context, Effect, Layer } from "effect"
@@ -33,12 +33,12 @@ const log = Log.create({ service: "llm" })
 export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
 
 export type StreamInput = {
-  user: SessionLegacy.User
+  user: SessionV1.User
   sessionID: string
   parentSessionID?: string
   model: Provider.Model
   agent: Agent.Info
-  permission?: PermissionLegacy.Ruleset
+  permission?: PermissionV1.Ruleset
   system: string[]
   messages: ModelMessage[]
   small?: boolean
@@ -165,7 +165,7 @@ const live: Layer.Layer<
             return { approved: true }
           }
 
-          const id = PermissionLegacy.ID.ascending()
+          const id = PermissionV1.ID.ascending()
           let unsub: EventV2.Unsubscribe | undefined
           try {
             unsub = await bridge.promise(

+ 4 - 4
packages/opencode/src/session/llm/request.ts

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import type { Auth } from "@/auth"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import type { RuntimeFlags } from "@/effect/runtime-flags"
 import { InstanceState } from "@/effect/instance-state"
 import { Permission } from "@/permission"
@@ -18,12 +18,12 @@ import { mergeDeep } from "remeda"
 const USER_AGENT = `opencode/${InstallationVersion}`
 
 type PrepareInput = {
-  readonly user: SessionLegacy.User
+  readonly user: SessionV1.User
   readonly sessionID: string
   readonly parentSessionID?: string
   readonly model: Provider.Model
   readonly agent: Agent.Info
-  readonly permission?: PermissionLegacy.Ruleset
+  readonly permission?: PermissionV1.Ruleset
   readonly system: string[]
   readonly messages: ModelMessage[]
   readonly small?: boolean

+ 6 - 6
packages/opencode/src/session/message-v2.ts

@@ -1,6 +1,6 @@
 import { EventV2 } from "@opencode-ai/core/event"
 import { SessionID, MessageID, PartID } from "./schema"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import {
   APIError,
@@ -17,7 +17,7 @@ import {
   User,
   WithParts,
   type ToolPart,
-} from "@opencode-ai/core/session/legacy"
+} from "@opencode-ai/core/v1/session"
 
 import { NamedError } from "@opencode-ai/core/util/error"
 import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
@@ -56,9 +56,9 @@ function truncateToolOutput(text: string, maxChars?: number) {
 }
 
 export const Event = {
-  Updated: SessionLegacy.Event.MessageUpdated,
-  Removed: SessionLegacy.Event.MessageRemoved,
-  PartUpdated: SessionLegacy.Event.PartUpdated,
+  Updated: SessionV1.Event.MessageUpdated,
+  Removed: SessionV1.Event.MessageRemoved,
+  PartUpdated: SessionV1.Event.PartUpdated,
   PartDelta: EventV2.define({
     type: "message.part.delta",
     schema: {
@@ -69,7 +69,7 @@ export const Event = {
       delta: Schema.String,
     },
   }),
-  PartRemoved: SessionLegacy.Event.PartRemoved,
+  PartRemoved: SessionV1.Event.PartRemoved,
 }
 
 const Cursor = Schema.Struct({

+ 5 - 4
packages/opencode/src/session/overflow.ts

@@ -1,12 +1,13 @@
 import type { Config } from "@/config/config"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import type { Provider } from "@/provider/provider"
 import { ProviderTransform } from "@/provider/transform"
 import type { MessageV2 } from "./message-v2"
 
 const COMPACTION_BUFFER = 20_000
 
-export function usable(input: { cfg: Config.Info; model: Provider.Model; outputTokenMax?: number }) {
+export function usable(input: { cfg: ConfigV1.Info; model: Provider.Model; outputTokenMax?: number }) {
   const context = input.model.limit.context
   if (context === 0) return 0
 
@@ -19,8 +20,8 @@ export function usable(input: { cfg: Config.Info; model: Provider.Model; outputT
 }
 
 export function isOverflow(input: {
-  cfg: Config.Info
-  tokens: SessionLegacy.Assistant["tokens"]
+  cfg: ConfigV1.Info
+  tokens: SessionV1.Assistant["tokens"]
   model: Provider.Model
   outputTokenMax?: number
 }) {

+ 21 - 21
packages/opencode/src/session/processor.ts

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Image } from "@/image/image"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Cause, Deferred, Effect, Exit, Layer, Context, Scope, Schema } from "effect"
 import * as Stream from "effect/Stream"
 import { Agent } from "@/agent/agent"
@@ -37,25 +37,25 @@ const log = Log.create({ service: "session.processor" })
 export type Result = "compact" | "stop" | "continue"
 
 export interface Handle {
-  readonly message: SessionLegacy.Assistant
+  readonly message: SessionV1.Assistant
   readonly updateToolCall: (
     toolCallID: string,
-    update: (part: SessionLegacy.ToolPart) => SessionLegacy.ToolPart,
-  ) => Effect.Effect<SessionLegacy.ToolPart | undefined>
+    update: (part: SessionV1.ToolPart) => SessionV1.ToolPart,
+  ) => Effect.Effect<SessionV1.ToolPart | undefined>
   readonly completeToolCall: (
     toolCallID: string,
     output: {
       title: string
       metadata: Record<string, any>
       output: string
-      attachments?: SessionLegacy.FilePart[]
+      attachments?: SessionV1.FilePart[]
     },
   ) => Effect.Effect<void>
   readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
 }
 
 type Input = {
-  assistantMessage: SessionLegacy.Assistant
+  assistantMessage: SessionV1.Assistant
   sessionID: SessionID
   model: Provider.Model
 }
@@ -65,9 +65,9 @@ export interface Interface {
 }
 
 type ToolCall = {
-  partID: SessionLegacy.ToolPart["id"]
-  messageID: SessionLegacy.ToolPart["messageID"]
-  sessionID: SessionLegacy.ToolPart["sessionID"]
+  partID: SessionV1.ToolPart["id"]
+  messageID: SessionV1.ToolPart["messageID"]
+  sessionID: SessionV1.ToolPart["sessionID"]
   done: Deferred.Deferred<void>
   inputEnded: boolean
 }
@@ -78,8 +78,8 @@ interface ProcessorContext extends Input {
   snapshot: string | undefined
   blocked: boolean
   needsCompaction: boolean
-  currentText: SessionLegacy.TextPart | undefined
-  reasoningMap: Record<string, SessionLegacy.ReasoningPart>
+  currentText: SessionV1.TextPart | undefined
+  reasoningMap: Record<string, SessionV1.ReasoningPart>
 }
 
 type StreamEvent = LLMEvent
@@ -153,7 +153,7 @@ export const layer = Layer.effect(
 
       const updateToolCall = Effect.fn("SessionProcessor.updateToolCall")(function* (
         toolCallID: string,
-        update: (part: SessionLegacy.ToolPart) => SessionLegacy.ToolPart,
+        update: (part: SessionV1.ToolPart) => SessionV1.ToolPart,
       ) {
         const match = yield* readToolCall(toolCallID)
         if (!match) return undefined
@@ -173,7 +173,7 @@ export const layer = Layer.effect(
           title: string
           metadata: Record<string, any>
           output: string
-          attachments?: SessionLegacy.FilePart[]
+          attachments?: SessionV1.FilePart[]
         },
       ) {
         const match = yield* readToolCall(toolCallID)
@@ -205,7 +205,7 @@ export const layer = Layer.effect(
             time: { start: match.part.state.time.start, end: Date.now() },
           },
         })
-        if (error instanceof PermissionLegacy.RejectedError || error instanceof Question.RejectedError) {
+        if (error instanceof PermissionV1.RejectedError || error instanceof Question.RejectedError) {
           ctx.blocked = ctx.shouldBreak
         }
         yield* settleToolCall(toolCallID)
@@ -268,7 +268,7 @@ export const layer = Layer.effect(
           callID: input.id,
           state: { status: "pending", input: {}, raw: "" },
           metadata: input.providerExecuted ? { providerExecuted: true } : undefined,
-        } satisfies SessionLegacy.ToolPart)
+        } satisfies SessionV1.ToolPart)
         ctx.toolcalls[input.id] = {
           done: yield* Deferred.make<void>(),
           partID: part.id,
@@ -279,11 +279,11 @@ export const layer = Layer.effect(
         return { call: ctx.toolcalls[input.id], part }
       })
 
-      const isFilePart = (value: unknown): value is SessionLegacy.FilePart => Schema.is(SessionLegacy.FilePart)(value)
+      const isFilePart = (value: unknown): value is SessionV1.FilePart => Schema.is(SessionV1.FilePart)(value)
 
       const toolResultOutput = (
         value: Extract<StreamEvent, { type: "tool-result" }>,
-      ): { title: string; metadata: Record<string, any>; output: string; attachments?: SessionLegacy.FilePart[] } => {
+      ): { title: string; metadata: Record<string, any>; output: string; attachments?: SessionV1.FilePart[] } => {
         if (isRecord(value.result.value) && typeof value.result.value.output === "string") {
           return {
             title: typeof value.result.value.title === "string" ? value.result.value.title : value.name,
@@ -463,7 +463,7 @@ export const layer = Layer.effect(
                     ),
                     Effect.exit,
                   )
-                : Effect.succeed(Exit.succeed<SessionLegacy.FilePart>(attachment)),
+                : Effect.succeed(Exit.succeed<SessionV1.FilePart>(attachment)),
             )
             const omitted = normalized.filter(Exit.isFailure).length
             const attachments = normalized.filter(Exit.isSuccess).map((item) => item.value)
@@ -486,7 +486,7 @@ export const layer = Layer.effect(
                     type: "text",
                     text: output.output,
                   },
-                  ...(output.attachments?.map((item: SessionLegacy.FilePart) => ({
+                  ...(output.attachments?.map((item: SessionV1.FilePart) => ({
                     type: "file" as const,
                     uri: item.url,
                     mime: item.mime,
@@ -753,7 +753,7 @@ export const layer = Layer.effect(
       const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
         slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
         const error = parse(e)
-        if (SessionLegacy.ContextOverflowError.isInstance(error)) {
+        if (SessionV1.ContextOverflowError.isInstance(error)) {
           ctx.needsCompaction = true
           yield* events.publish(Session.Event.Error, { sessionID: ctx.sessionID, error })
           return

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

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import path from "path"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import os from "os"
 import { SessionID, MessageID, PartID } from "./schema"
 import { MessageV2 } from "./message-v2"
@@ -66,8 +66,8 @@ import { LLMEvent } from "@opencode-ai/llm"
 // @ts-ignore
 globalThis.AI_SDK_LOG_WARNINGS = false
 
-const decodeMessageInfo = Schema.decodeUnknownExit(SessionLegacy.Info)
-const decodeMessagePart = Schema.decodeUnknownExit(SessionLegacy.Part)
+const decodeMessageInfo = Schema.decodeUnknownExit(SessionV1.Info)
+const decodeMessagePart = Schema.decodeUnknownExit(SessionV1.Part)
 
 const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
 
@@ -82,7 +82,7 @@ const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested struc
 const log = Log.create({ service: "session.prompt" })
 const elog = EffectLogger.create({ service: "session.prompt" })
 
-function isOrphanedInterruptedTool(part: SessionLegacy.ToolPart) {
+function isOrphanedInterruptedTool(part: SessionV1.ToolPart) {
   // cleanup() marks abandoned tool_use blocks this way after retries/aborts.
   // They are not pending work and must not trigger an assistant-prefill request.
   return part.state.status === "error" && part.state.metadata?.interrupted === true
@@ -90,10 +90,10 @@ function isOrphanedInterruptedTool(part: SessionLegacy.ToolPart) {
 
 export interface Interface {
   readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
-  readonly prompt: (input: PromptInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error>
-  readonly loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts>
-  readonly shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError>
-  readonly command: (input: CommandInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error>
+  readonly prompt: (input: PromptInput) => Effect.Effect<SessionV1.WithParts, Image.Error>
+  readonly loop: (input: LoopInput) => Effect.Effect<SessionV1.WithParts>
+  readonly shell: (input: ShellInput) => Effect.Effect<SessionV1.WithParts, Session.BusyError>
+  readonly command: (input: CommandInput) => Effect.Effect<SessionV1.WithParts, Image.Error>
   readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
 }
 
@@ -239,14 +239,14 @@ export const layer = Layer.effect(
 
     const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: {
       session: Session.Info
-      history: SessionLegacy.WithParts[]
+      history: SessionV1.WithParts[]
       providerID: ProviderV2.ID
       modelID: ProviderV2.ModelID
     }) {
       if (input.session.parentID) return
       if (!Session.isDefaultTitle(input.session.title)) return
 
-      const real = (m: SessionLegacy.WithParts) =>
+      const real = (m: SessionV1.WithParts) =>
         m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
       const idx = input.history.findIndex(real)
       if (idx === -1) return
@@ -257,7 +257,7 @@ export const layer = Layer.effect(
       if (!firstUser || firstUser.info.role !== "user") return
       const firstInfo = firstUser.info
 
-      const subtasks = firstUser.parts.filter((p): p is SessionLegacy.SubtaskPart => p.type === "subtask")
+      const subtasks = firstUser.parts.filter((p): p is SessionV1.SubtaskPart => p.type === "subtask")
       const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask")
 
       const ag = yield* agents.get("title")
@@ -300,19 +300,19 @@ export const layer = Layer.effect(
     })
 
     const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
-      task: SessionLegacy.SubtaskPart
+      task: SessionV1.SubtaskPart
       model: Provider.Model
-      lastUser: SessionLegacy.User
+      lastUser: SessionV1.User
       sessionID: SessionID
       session: Session.Info
-      msgs: SessionLegacy.WithParts[]
+      msgs: SessionV1.WithParts[]
     }) {
       const { task, model, lastUser, sessionID, session, msgs } = input
       const ctx = yield* InstanceState.context
       const promptOps = yield* ops()
       const { task: taskTool } = yield* registry.named()
       const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
-      const assistantMessage: SessionLegacy.Assistant = yield* sessions.updateMessage({
+      const assistantMessage: SessionV1.Assistant = yield* sessions.updateMessage({
         id: MessageID.ascending(),
         role: "assistant",
         parentID: lastUser.id,
@@ -327,7 +327,7 @@ export const layer = Layer.effect(
         providerID: taskModel.providerID,
         time: { created: Date.now() },
       })
-      let part: SessionLegacy.ToolPart = yield* sessions.updatePart({
+      let part: SessionV1.ToolPart = yield* sessions.updatePart({
         id: PartID.ascending(),
         messageID: assistantMessage.id,
         sessionID: assistantMessage.sessionID,
@@ -383,7 +383,7 @@ export const layer = Layer.effect(
                 ...part,
                 type: "tool",
                 state: { ...part.state, ...val },
-              } satisfies SessionLegacy.ToolPart)
+              } satisfies SessionV1.ToolPart)
             }),
           ask: (req: any) =>
             permission
@@ -417,7 +417,7 @@ export const layer = Layer.effect(
                     metadata: part.state.metadata,
                     input: part.state.input,
                   },
-                } satisfies SessionLegacy.ToolPart)
+                } satisfies SessionV1.ToolPart)
               }
             }),
           ),
@@ -452,7 +452,7 @@ export const layer = Layer.effect(
             attachments,
             time: { ...part.state.time, end: Date.now() },
           },
-        } satisfies SessionLegacy.ToolPart)
+        } satisfies SessionV1.ToolPart)
       }
 
       if (!result) {
@@ -468,12 +468,12 @@ export const layer = Layer.effect(
             metadata: part.state.status === "pending" ? undefined : part.state.metadata,
             input: part.state.input,
           },
-        } satisfies SessionLegacy.ToolPart)
+        } satisfies SessionV1.ToolPart)
       }
 
       if (!task.command) return
 
-      const summaryUserMsg: SessionLegacy.User = {
+      const summaryUserMsg: SessionV1.User = {
         id: MessageID.ascending(),
         sessionID,
         role: "user",
@@ -489,7 +489,7 @@ export const layer = Layer.effect(
         type: "text",
         text: "Summarize the task tool output above and continue with your task.",
         synthetic: true,
-      } satisfies SessionLegacy.TextPart)
+      } satisfies SessionV1.TextPart)
     })
 
     const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput, ready?: Latch.Latch) {
@@ -511,7 +511,7 @@ export const layer = Layer.effect(
               throw error
             }
             const model = input.model ?? agent.model ?? (yield* currentModel(input.sessionID))
-            const userMsg: SessionLegacy.User = {
+            const userMsg: SessionV1.User = {
               id: input.messageID ?? MessageID.ascending(),
               sessionID: input.sessionID,
               time: { created: Date.now() },
@@ -520,7 +520,7 @@ export const layer = Layer.effect(
               model: { providerID: model.providerID, modelID: model.modelID },
             }
             yield* sessions.updateMessage(userMsg)
-            const userPart: SessionLegacy.Part = {
+            const userPart: SessionV1.Part = {
               type: "text",
               id: PartID.ascending(),
               messageID: userMsg.id,
@@ -530,7 +530,7 @@ export const layer = Layer.effect(
             }
             yield* sessions.updatePart(userPart)
 
-            const msg: SessionLegacy.Assistant = {
+            const msg: SessionV1.Assistant = {
               id: MessageID.ascending(),
               sessionID: input.sessionID,
               parentID: userMsg.id,
@@ -546,7 +546,7 @@ export const layer = Layer.effect(
             }
             yield* sessions.updateMessage(msg)
             const started = Date.now()
-            const part: SessionLegacy.ToolPart = {
+            const part: SessionV1.ToolPart = {
               type: "tool",
               id: PartID.ascending(),
               messageID: msg.id,
@@ -719,7 +719,7 @@ export const layer = Layer.effect(
           : undefined
       const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
 
-      const info: SessionLegacy.User = {
+      const info: SessionV1.User = {
         id: input.messageID ?? MessageID.ascending(),
         role: "user",
         sessionID: input.sessionID,
@@ -760,8 +760,8 @@ export const layer = Layer.effect(
 
       yield* Effect.addFinalizer(() => instruction.clear(info.id))
 
-      type Draft<T> = T extends SessionLegacy.Part ? Omit<T, "id"> & { id?: string } : never
-      const assign = (part: Draft<SessionLegacy.Part>): SessionLegacy.Part => ({
+      type Draft<T> = T extends SessionV1.Part ? Omit<T, "id"> & { id?: string } : never
+      const assign = (part: Draft<SessionV1.Part>): SessionV1.Part => ({
         ...part,
         id: part.id ? PartID.make(part.id) : PartID.ascending(),
       })
@@ -790,14 +790,14 @@ export const layer = Layer.effect(
         })
       })
 
-      const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionLegacy.Part>[]> = Effect.fn(
+      const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionV1.Part>[]> = Effect.fn(
         "SessionPrompt.resolveUserPart",
       )(function* (part) {
         if (part.type === "file") {
           if (part.source?.type === "resource") {
             const { clientName, uri } = part.source
             log.info("mcp resource", { clientName, uri, mime: part.mime })
-            const pieces: Draft<SessionLegacy.Part>[] = [
+            const pieces: Draft<SessionV1.Part>[] = [
               {
                 messageID: info.id,
                 sessionID: input.sessionID,
@@ -917,7 +917,7 @@ export const layer = Layer.effect(
                   if (end) limit = end - (offset - 1)
                 }
                 const args = { filePath: filepath, offset, limit }
-                const pieces: Draft<SessionLegacy.Part>[] = [
+                const pieces: Draft<SessionV1.Part>[] = [
                   ...(referenceContext
                     ? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
                     : []),
@@ -1213,7 +1213,7 @@ export const layer = Layer.effect(
       return { info, parts }
     }, Effect.scoped)
 
-    const prompt: (input: PromptInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error> = Effect.fn(
+    const prompt: (input: PromptInput) => Effect.Effect<SessionV1.WithParts, Image.Error> = Effect.fn(
       "SessionPrompt.prompt",
     )(function* (input: PromptInput) {
       const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
@@ -1221,7 +1221,7 @@ export const layer = Layer.effect(
       const message = yield* createUserMessage(input)
       yield* sessions.touch(input.sessionID)
 
-      const permissions: PermissionLegacy.Rule[] = []
+      const permissions: PermissionV1.Rule[] = []
       for (const [t, enabled] of Object.entries(input.tools ?? {})) {
         permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
       }
@@ -1242,7 +1242,7 @@ export const layer = Layer.effect(
       throw new Error("Impossible")
     })
 
-    const runLoop: (sessionID: SessionID) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.run")(
+    const runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.run")(
       function* (sessionID: SessionID) {
         const ctx = yield* InstanceState.context
         const slog = elog.with({ sessionID })
@@ -1280,7 +1280,7 @@ export const layer = Layer.effect(
             lastUser.id < lastAssistant.id
           ) {
             const orphan = lastAssistantMsg?.parts.find(
-              (part): part is SessionLegacy.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
+              (part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
             )
             if (orphan) {
               yield* slog.warn("loop exit with orphaned interrupted tool", {
@@ -1347,7 +1347,7 @@ export const layer = Layer.effect(
             Effect.provideService(Session.Service, sessions),
           )
 
-          const msg: SessionLegacy.Assistant = {
+          const msg: SessionV1.Assistant = {
             id: MessageID.ascending(),
             parentID: lastUser.id,
             role: "assistant",
@@ -1467,7 +1467,7 @@ export const layer = Layer.effect(
             const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
             if (finished && !handle.message.error) {
               if (format.type === "json_schema") {
-                handle.message.error = new SessionLegacy.StructuredOutputError({
+                handle.message.error = new SessionV1.StructuredOutputError({
                   message: "Model did not produce structured output",
                   retries: 0,
                 }).toObject()
@@ -1500,13 +1500,13 @@ export const layer = Layer.effect(
       },
     )
 
-    const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(
+    const loop: (input: LoopInput) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.loop")(
       function* (input: LoopInput) {
         return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
       },
     )
 
-    const shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError> = Effect.fn(
+    const shell: (input: ShellInput) => Effect.Effect<SessionV1.WithParts, Session.BusyError> = Effect.fn(
       "SessionPrompt.shell",
     )(function* (input: ShellInput) {
       const ready = yield* Latch.make()
@@ -1691,15 +1691,15 @@ export const PromptInput = Schema.Struct({
     description:
       "@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
   }),
-  format: Schema.optional(SessionLegacy.Format),
+  format: Schema.optional(SessionV1.Format),
   system: Schema.optional(Schema.String),
   variant: Schema.optional(Schema.String),
   parts: Schema.Array(
     Schema.Union([
-      SessionLegacy.TextPartInput,
-      SessionLegacy.FilePartInput,
-      SessionLegacy.AgentPartInput,
-      SessionLegacy.SubtaskPartInput,
+      SessionV1.TextPartInput,
+      SessionV1.FilePartInput,
+      SessionV1.AgentPartInput,
+      SessionV1.SubtaskPartInput,
     ]).annotate({ discriminator: "type" }),
   ),
 })
@@ -1738,7 +1738,7 @@ export const CommandInput = Schema.Struct({
           mime: Schema.String,
           filename: Schema.optional(Schema.String),
           url: Schema.String,
-          source: Schema.optional(SessionLegacy.FilePartSource),
+          source: Schema.optional(SessionV1.FilePartSource),
         }),
       ]).annotate({ discriminator: "type" }),
     ),

+ 2 - 2
packages/opencode/src/session/prompt/reference.ts

@@ -1,5 +1,5 @@
 import { Option, Schema } from "effect"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { MessageV2 } from "../message-v2"
 import { Reference } from "@/reference/reference"
 
@@ -34,7 +34,7 @@ export function referenceTextPart(input: {
   target?: string
   targetPath?: string
   problem?: string
-}): SessionLegacy.TextPartInput {
+}): SessionV1.TextPartInput {
   const metadata: ReferencePromptMetadata = {
     name: input.reference.name,
     kind: input.reference.kind,

+ 2 - 2
packages/opencode/src/session/reminders.ts

@@ -1,5 +1,5 @@
 import path from "path"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Effect } from "effect"
 import { Agent } from "@/agent/agent"
 import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -13,7 +13,7 @@ import BUILD_SWITCH from "./prompt/build-switch.txt"
 import PLAN_MODE from "./prompt/plan-mode.txt"
 
 export const apply = Effect.fn("SessionReminders.apply")(function* (input: {
-  messages: SessionLegacy.WithParts[]
+  messages: SessionV1.WithParts[]
   agent: Agent.Info
   session: Session.Info
 }) {

+ 5 - 5
packages/opencode/src/session/retry.ts

@@ -1,5 +1,5 @@
 import type { NamedError } from "@opencode-ai/core/util/error"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Cause, Clock, Duration, Effect, Schedule } from "effect"
 import { MessageV2 } from "./message-v2"
 import { iife } from "@/util/iife"
@@ -32,7 +32,7 @@ function cap(ms: number) {
   return Math.min(ms, RETRY_MAX_DELAY)
 }
 
-export function delay(attempt: number, error?: SessionLegacy.APIError) {
+export function delay(attempt: number, error?: SessionV1.APIError) {
   if (error) {
     const headers = error.data.responseHeaders
     if (headers) {
@@ -67,8 +67,8 @@ export function delay(attempt: number, error?: SessionLegacy.APIError) {
 
 export function retryable(error: Err, provider: string) {
   // context overflow errors should not be retried
-  if (SessionLegacy.ContextOverflowError.isInstance(error)) return undefined
-  if (SessionLegacy.APIError.isInstance(error)) {
+  if (SessionV1.ContextOverflowError.isInstance(error)) return undefined
+  if (SessionV1.APIError.isInstance(error)) {
     const status = error.data.statusCode
     // 5xx errors are transient server failures and should always be retried,
     // even when the provider SDK doesn't explicitly mark them as retryable.
@@ -184,7 +184,7 @@ export function policy(opts: {
       const retry = retryable(error, opts.provider)
       if (!retry) return Cause.done(meta.attempt)
       return Effect.gen(function* () {
-        const wait = delay(meta.attempt, SessionLegacy.APIError.isInstance(error) ? error : undefined)
+        const wait = delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined)
         const now = yield* Clock.currentTimeMillis
         yield* opts.set({
           attempt: meta.attempt,

+ 4 - 4
packages/opencode/src/session/revert.ts

@@ -1,5 +1,5 @@
 import { Effect, Layer, Context, Schema } from "effect"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { EventV2Bridge } from "@/event-v2-bridge"
 import { Snapshot } from "../snapshot"
 import { Storage } from "@/storage/storage"
@@ -40,7 +40,7 @@ export const layer = Layer.effect(
     const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) {
       yield* state.assertNotBusy(input.sessionID)
       const all = yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)
-      let lastUser: SessionLegacy.User | undefined
+      let lastUser: SessionV1.User | undefined
       const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
 
       let rev: Session.Info["revert"]
@@ -104,8 +104,8 @@ export const layer = Layer.effect(
       const sessionID = session.id
       const msgs = yield* sessions.messages({ sessionID }).pipe(Effect.orDie)
       const messageID = session.revert.messageID
-      const remove = [] as SessionLegacy.WithParts[]
-      let target: SessionLegacy.WithParts | undefined
+      const remove = [] as SessionV1.WithParts[]
+      let target: SessionV1.WithParts | undefined
       for (const msg of msgs) {
         if (msg.info.id < messageID) continue
         if (msg.info.id > messageID) {

+ 14 - 14
packages/opencode/src/session/run-state.ts

@@ -1,5 +1,5 @@
 import { InstanceState } from "@/effect/instance-state"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Runner } from "@/effect/runner"
 import { BackgroundJob } from "@/background/job"
 import { Effect, Latch, Layer, Scope, Context } from "effect"
@@ -13,15 +13,15 @@ export interface Interface {
   readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
   readonly ensureRunning: (
     sessionID: SessionID,
-    onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
-    work: Effect.Effect<SessionLegacy.WithParts>,
-  ) => Effect.Effect<SessionLegacy.WithParts>
+    onInterrupt: Effect.Effect<SessionV1.WithParts>,
+    work: Effect.Effect<SessionV1.WithParts>,
+  ) => Effect.Effect<SessionV1.WithParts>
   readonly startShell: (
     sessionID: SessionID,
-    onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
-    work: Effect.Effect<SessionLegacy.WithParts>,
+    onInterrupt: Effect.Effect<SessionV1.WithParts>,
+    work: Effect.Effect<SessionV1.WithParts>,
     ready?: Latch.Latch,
-  ) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError>
+  ) => Effect.Effect<SessionV1.WithParts, Session.BusyError>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunState") {}
@@ -35,7 +35,7 @@ export const layer = Layer.effect(
     const state = yield* InstanceState.make(
       Effect.fn("SessionRunState.state")(function* () {
         const scope = yield* Scope.Scope
-        const runners = new Map<SessionID, Runner.Runner<SessionLegacy.WithParts>>()
+        const runners = new Map<SessionID, Runner.Runner<SessionV1.WithParts>>()
         yield* Effect.addFinalizer(
           Effect.fnUntraced(function* () {
             yield* Effect.forEach(runners.values(), (runner) => runner.cancel, {
@@ -51,12 +51,12 @@ export const layer = Layer.effect(
 
     const runner = Effect.fn("SessionRunState.runner")(function* (
       sessionID: SessionID,
-      onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
+      onInterrupt: Effect.Effect<SessionV1.WithParts>,
     ) {
       const data = yield* InstanceState.get(state)
       const existing = data.runners.get(sessionID)
       if (existing) return existing
-      const next = Runner.make<SessionLegacy.WithParts>(data.scope, {
+      const next = Runner.make<SessionV1.WithParts>(data.scope, {
         onIdle: Effect.gen(function* () {
           data.runners.delete(sessionID)
           yield* status.set(sessionID, { type: "idle" })
@@ -87,16 +87,16 @@ export const layer = Layer.effect(
 
     const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* (
       sessionID: SessionID,
-      onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
-      work: Effect.Effect<SessionLegacy.WithParts>,
+      onInterrupt: Effect.Effect<SessionV1.WithParts>,
+      work: Effect.Effect<SessionV1.WithParts>,
     ) {
       return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work)
     })
 
     const startShell = Effect.fn("SessionRunState.startShell")(function* (
       sessionID: SessionID,
-      onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
-      work: Effect.Effect<SessionLegacy.WithParts>,
+      onInterrupt: Effect.Effect<SessionV1.WithParts>,
+      work: Effect.Effect<SessionV1.WithParts>,
       ready?: Latch.Latch,
     ) {
       return yield* (yield* runner(sessionID, onInterrupt))

+ 35 - 35
packages/opencode/src/session/session.ts

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Slug } from "@opencode-ai/core/util/slug"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { serviceUse } from "@opencode-ai/core/effect/service-use"
 import path from "path"
 import { BackgroundJob } from "@/background/job"
@@ -234,7 +234,7 @@ export const Info = Schema.Struct({
   version: Schema.String,
   metadata: optionalOmitUndefined(Metadata),
   time: Time,
-  permission: optionalOmitUndefined(PermissionLegacy.Ruleset),
+  permission: optionalOmitUndefined(PermissionV1.Ruleset),
   revert: optionalOmitUndefined(Revert),
 }).annotate({ identifier: "Session" })
 export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
@@ -259,7 +259,7 @@ export const CreateInput = Schema.optional(
     agent: Schema.optional(Schema.String),
     model: Schema.optional(Model),
     metadata: Schema.optional(Metadata),
-    permission: Schema.optional(PermissionLegacy.Ruleset),
+    permission: Schema.optional(PermissionV1.Ruleset),
     workspaceID: Schema.optional(WorkspaceV2.ID),
   }),
 )
@@ -283,7 +283,7 @@ export const SetMetadataInput = Schema.Struct({
 })
 export const SetPermissionInput = Schema.Struct({
   sessionID: SessionID,
-  permission: PermissionLegacy.Ruleset,
+  permission: PermissionV1.Ruleset,
 })
 export const SetRevertInput = Schema.Struct({
   sessionID: SessionID,
@@ -349,7 +349,7 @@ const UpdatedInfo = Schema.Struct({
   version: Schema.optional(Schema.NullOr(Schema.String)),
   metadata: Schema.optional(Schema.NullOr(Metadata)),
   time: Schema.optional(UpdatedTime),
-  permission: Schema.optional(Schema.NullOr(PermissionLegacy.Ruleset)),
+  permission: Schema.optional(Schema.NullOr(PermissionV1.Ruleset)),
   revert: Schema.optional(Schema.NullOr(Revert)),
 })
 
@@ -359,9 +359,9 @@ const UpdatedEventSchema = Schema.Struct({
 })
 
 export const Event = {
-  Created: SessionLegacy.Event.Created,
-  Updated: SessionLegacy.Event.Updated,
-  Deleted: SessionLegacy.Event.Deleted,
+  Created: SessionV1.Event.Created,
+  Updated: SessionV1.Event.Updated,
+  Deleted: SessionV1.Event.Deleted,
   Diff: EventV2.define({
     type: "session.diff",
     schema: {
@@ -373,9 +373,9 @@ export const Event = {
     type: "session.error",
     schema: {
       sessionID: Schema.optional(SessionID),
-      // Reuses SessionLegacy.Assistant.fields.error (already Schema.optional) so
+      // Reuses SessionV1.Assistant.fields.error (already Schema.optional) so
       // the derived schema keeps the same discriminated-union shape on the event stream.
-      error: SessionLegacy.Assistant.fields.error,
+      error: SessionV1.Assistant.fields.error,
     },
   }),
 }
@@ -473,7 +473,7 @@ export interface Interface {
     agent?: string
     model?: Schema.Schema.Type<typeof Model>
     metadata?: typeof Metadata.Type
-    permission?: PermissionLegacy.Ruleset
+    permission?: PermissionV1.Ruleset
     workspaceID?: WorkspaceV2.ID
   }) => Effect.Effect<Info>
   readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info, NotFound>
@@ -482,7 +482,7 @@ export interface Interface {
   readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect<void>
   readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect<void>
   readonly setMetadata: (input: typeof SetMetadataInput.Type) => Effect.Effect<void>
-  readonly setPermission: (input: { sessionID: SessionID; permission: PermissionLegacy.Ruleset }) => Effect.Effect<void>
+  readonly setPermission: (input: { sessionID: SessionID; permission: PermissionV1.Ruleset }) => Effect.Effect<void>
   readonly setRevert: (input: {
     sessionID: SessionID
     revert: Info["revert"]
@@ -496,18 +496,18 @@ export interface Interface {
   readonly messages: (input: {
     sessionID: SessionID
     limit?: number
-  }) => Effect.Effect<SessionLegacy.WithParts[], NotFound>
+  }) => Effect.Effect<SessionV1.WithParts[], NotFound>
   readonly children: (parentID: SessionID) => Effect.Effect<Info[]>
   readonly remove: (sessionID: SessionID) => Effect.Effect<void, NotFound>
-  readonly updateMessage: <T extends SessionLegacy.Info>(msg: T) => Effect.Effect<T>
+  readonly updateMessage: <T extends SessionV1.Info>(msg: T) => Effect.Effect<T>
   readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<MessageID>
   readonly removePart: (input: { sessionID: SessionID; messageID: MessageID; partID: PartID }) => Effect.Effect<PartID>
   readonly getPart: (input: {
     sessionID: SessionID
     messageID: MessageID
     partID: PartID
-  }) => Effect.Effect<SessionLegacy.Part | undefined>
-  readonly updatePart: <T extends SessionLegacy.Part>(part: T) => Effect.Effect<T>
+  }) => Effect.Effect<SessionV1.Part | undefined>
+  readonly updatePart: <T extends SessionV1.Part>(part: T) => Effect.Effect<T>
   readonly updatePartDelta: (input: {
     sessionID: SessionID
     messageID: MessageID
@@ -518,8 +518,8 @@ export interface Interface {
   /** Finds the first message matching the predicate, searching newest-first. */
   readonly findMessage: (
     sessionID: SessionID,
-    predicate: (msg: SessionLegacy.WithParts) => boolean,
-  ) => Effect.Effect<Option.Option<SessionLegacy.WithParts>, NotFound>
+    predicate: (msg: SessionV1.WithParts) => boolean,
+  ) => Effect.Effect<Option.Option<SessionV1.WithParts>, NotFound>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
@@ -571,7 +571,7 @@ export const layer: Layer.Layer<
       directory: string
       path?: string
       metadata?: typeof Metadata.Type
-      permission?: PermissionLegacy.Ruleset
+      permission?: PermissionV1.Ruleset
     }) {
       const ctx = yield* InstanceState.context
       const result: Info = {
@@ -598,7 +598,7 @@ export const layer: Layer.Layer<
       log.info("created", result)
 
       yield* events.publish(
-        SessionLegacy.Event.Created,
+        SessionV1.Event.Created,
         { sessionID: result.id, info: result },
         { location: eventLocation(result) },
       )
@@ -689,7 +689,7 @@ export const layer: Layer.Layer<
         }
 
         yield* events.publish(
-          SessionLegacy.Event.Deleted,
+          SessionV1.Event.Deleted,
           { sessionID, info: session },
           { location: eventLocation(session) },
         )
@@ -699,18 +699,18 @@ export const layer: Layer.Layer<
       }
     })
 
-    const updateMessage = <T extends SessionLegacy.Info>(msg: T): Effect.Effect<T> =>
+    const updateMessage = <T extends SessionV1.Info>(msg: T): Effect.Effect<T> =>
       Effect.gen(function* () {
         const location = yield* locationForSession(msg.sessionID)
-        yield* events.publish(SessionLegacy.Event.MessageUpdated, { sessionID: msg.sessionID, info: msg }, { location })
+        yield* events.publish(SessionV1.Event.MessageUpdated, { sessionID: msg.sessionID, info: msg }, { location })
         return msg
       }).pipe(Effect.withSpan("Session.updateMessage"))
 
-    const updatePart = <T extends SessionLegacy.Part>(part: T): Effect.Effect<T> =>
+    const updatePart = <T extends SessionV1.Part>(part: T): Effect.Effect<T> =>
       Effect.gen(function* () {
         const location = yield* locationForSession(part.sessionID)
         yield* events.publish(
-          SessionLegacy.Event.PartUpdated,
+          SessionV1.Event.PartUpdated,
           {
             sessionID: part.sessionID,
             part: structuredClone(part),
@@ -740,7 +740,7 @@ export const layer: Layer.Layer<
         id: row.id,
         sessionID: row.session_id,
         messageID: row.message_id,
-      } as SessionLegacy.Part
+      } as SessionV1.Part
     })
 
     const create = Effect.fn("Session.create")(function* (input?: {
@@ -749,7 +749,7 @@ export const layer: Layer.Layer<
       agent?: string
       model?: Schema.Schema.Type<typeof Model>
       metadata?: typeof Metadata.Type
-      permission?: PermissionLegacy.Ruleset
+      permission?: PermissionV1.Ruleset
       workspaceID?: WorkspaceV2.ID
     }) {
       const ctx = yield* InstanceState.context
@@ -795,7 +795,7 @@ export const layer: Layer.Layer<
         })
 
         for (const part of msg.parts) {
-          const p: SessionLegacy.Part = {
+          const p: SessionV1.Part = {
             ...part,
             id: PartID.ascending(),
             messageID: cloned.id,
@@ -822,7 +822,7 @@ export const layer: Layer.Layer<
           revert: info.revert === null ? undefined : (info.revert ?? current.revert),
           permission: info.permission === null ? undefined : (info.permission ?? current.permission),
         } as Info
-        yield* events.publish(SessionLegacy.Event.Updated, { sessionID, info: next }, { location: eventLocation(next) })
+        yield* events.publish(SessionV1.Event.Updated, { sessionID, info: next }, { location: eventLocation(next) })
       })
 
     const touch = Effect.fn("Session.touch")(function* (sessionID: SessionID) {
@@ -843,7 +843,7 @@ export const layer: Layer.Layer<
 
     const setPermission = Effect.fn("Session.setPermission")(function* (input: {
       sessionID: SessionID
-      permission: PermissionLegacy.Ruleset
+      permission: PermissionV1.Ruleset
     }) {
       yield* patch(input.sessionID, { permission: [...input.permission], time: { updated: Date.now() } }).pipe(
         Effect.orDie,
@@ -899,7 +899,7 @@ export const layer: Layer.Layer<
       }
 
       const size = 50
-      const result = [] as SessionLegacy.WithParts[]
+      const result = [] as SessionV1.WithParts[]
       let before: string | undefined
       while (true) {
         const page = yield* MessageV2.page({ sessionID: input.sessionID, limit: size, before }).pipe(
@@ -922,7 +922,7 @@ export const layer: Layer.Layer<
     }) {
       const location = yield* locationForSession(input.sessionID)
       yield* events.publish(
-        SessionLegacy.Event.MessageRemoved,
+        SessionV1.Event.MessageRemoved,
         {
           sessionID: input.sessionID,
           messageID: input.messageID,
@@ -939,7 +939,7 @@ export const layer: Layer.Layer<
     }) {
       const location = yield* locationForSession(input.sessionID)
       yield* events.publish(
-        SessionLegacy.Event.PartRemoved,
+        SessionV1.Event.PartRemoved,
         {
           sessionID: input.sessionID,
           messageID: input.messageID,
@@ -976,7 +976,7 @@ export const layer: Layer.Layer<
         if (!page.more || !page.cursor) break
         before = page.cursor
       }
-      return Option.none<SessionLegacy.WithParts>()
+      return Option.none<SessionV1.WithParts>()
     })
 
     return Service.of({

+ 3 - 3
packages/opencode/src/session/summary.ts

@@ -1,5 +1,5 @@
 import { Effect, Layer, Context, Schema } from "effect"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { EventV2Bridge } from "@/event-v2-bridge"
 import { Snapshot } from "@/snapshot"
 import { Session } from "./session"
@@ -65,7 +65,7 @@ function unquoteGitPath(input: string) {
 export interface Interface {
   readonly summarize: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<void>
   readonly diff: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Snapshot.FileDiff[]>
-  readonly computeDiff: (input: { messages: SessionLegacy.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
+  readonly computeDiff: (input: { messages: SessionV1.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/SessionSummary") {}
@@ -79,7 +79,7 @@ export const layer = Layer.effect(
     const config = yield* Config.Service
 
     const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: {
-      messages: SessionLegacy.WithParts[]
+      messages: SessionV1.WithParts[]
     }) {
       let from: string | undefined
       let to: string | undefined

+ 3 - 3
packages/opencode/src/session/tools.ts

@@ -1,5 +1,5 @@
 import { Agent } from "@/agent/agent"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Provider } from "@/provider/provider"
 import { ProviderTransform } from "@/provider/transform"
 import { MCP } from "@/mcp"
@@ -29,7 +29,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
   session: Session.Info
   processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
   bypassAgentCheck: boolean
-  messages: SessionLegacy.WithParts[]
+  messages: SessionV1.WithParts[]
   promptOps: TaskPromptOps
 }) {
   using _ = log.time("resolveTools")
@@ -153,7 +153,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
           )
 
           const textParts: string[] = []
-          const attachments: Omit<SessionLegacy.FilePart, "id" | "sessionID" | "messageID">[] = []
+          const attachments: Omit<SessionV1.FilePart, "id" | "sessionID" | "messageID">[] = []
           for (const contentItem of result.content) {
             if (contentItem.type === "text") textParts.push(contentItem.text)
             else if (contentItem.type === "image") {

+ 2 - 1
packages/opencode/src/skill/index.ts

@@ -9,6 +9,7 @@ import { Global } from "@opencode-ai/core/global"
 import { Permission } from "@/permission"
 import { FSUtil } from "@opencode-ai/core/fs-util"
 import { Config } from "@/config/config"
+import { FrontmatterError } from "@opencode-ai/core/v1/config/error"
 import { ConfigMarkdown } from "@/config/markdown"
 import { RuntimeFlags } from "@/effect/runtime-flags"
 import { Glob } from "@opencode-ai/core/util/glob"
@@ -108,7 +109,7 @@ const add = Effect.fnUntraced(function* (state: State, match: string, events: Ev
   }).pipe(
     Effect.catch(
       Effect.fnUntraced(function* (err) {
-        const message = ConfigMarkdown.FrontmatterError.isInstance(err)
+        const message = FrontmatterError.isInstance(err)
           ? err.data.message
           : `Failed to parse skill ${match}`
         const { Session } = yield* Effect.promise(() => import("@/session/session"))

+ 3 - 3
packages/opencode/src/tool/plan.ts

@@ -1,5 +1,5 @@
 import path from "path"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Effect, Schema } from "effect"
 import * as Tool from "./tool"
 import { Question } from "../question"
@@ -50,7 +50,7 @@ export const PlanExitTool = Tool.define(
           const model =
             lastUser?.info.role === "user" && lastUser.info.model ? lastUser.info.model : yield* provider.defaultModel()
 
-          const msg: SessionLegacy.User = {
+          const msg: SessionV1.User = {
             id: MessageID.ascending(),
             sessionID: ctx.sessionID,
             role: "user",
@@ -66,7 +66,7 @@ export const PlanExitTool = Tool.define(
             type: "text",
             text: `The plan at ${plan} has been approved, you can now edit files. Execute the plan`,
             synthetic: true,
-          } satisfies SessionLegacy.TextPart)
+          } satisfies SessionV1.TextPart)
 
           return {
             title: "Switching to build agent",

+ 2 - 2
packages/opencode/src/tool/task.ts

@@ -1,7 +1,7 @@
 import * as Tool from "./tool"
 import DESCRIPTION from "./task.txt"
 import { ToolJsonSchema } from "./json-schema"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { BackgroundJob } from "@/background/job"
 import { Session } from "@/session/session"
 import { SessionID, MessageID } from "../session/schema"
@@ -18,7 +18,7 @@ import { Database } from "@opencode-ai/core/database/database"
 export interface TaskPromptOps {
   cancel(sessionID: SessionID): Effect.Effect<void>
   resolvePromptParts(template: string): Effect.Effect<SessionPrompt.PromptInput["parts"]>
-  prompt(input: SessionPrompt.PromptInput): Effect.Effect<SessionLegacy.WithParts>
+  prompt(input: SessionPrompt.PromptInput): Effect.Effect<SessionV1.WithParts>
 }
 
 const id = "task"

+ 5 - 5
packages/opencode/src/tool/tool.ts

@@ -1,6 +1,6 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Effect, Schema } from "effect"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import type { JSONSchema7 } from "@ai-sdk/provider"
 import type { MessageV2 } from "../session/message-v2"
 import type { Permission } from "../permission"
@@ -40,16 +40,16 @@ export type Context<M extends Metadata = Metadata> = {
   abort: AbortSignal
   callID?: string
   extra?: { [key: string]: unknown }
-  messages: SessionLegacy.WithParts[]
+  messages: SessionV1.WithParts[]
   metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
-  ask(input: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
+  ask(input: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
 }
 
 export interface ExecuteResult<M extends Metadata = Metadata> {
   title: string
   metadata: M
   output: string
-  attachments?: Omit<SessionLegacy.FilePart, "id" | "sessionID" | "messageID">[]
+  attachments?: Omit<SessionV1.FilePart, "id" | "sessionID" | "messageID">[]
 }
 
 export interface Def<

+ 2 - 2
packages/opencode/test/agent/agent.test.ts

@@ -9,7 +9,7 @@ import { Config } from "../../src/config/config"
 import { RuntimeFlags } from "../../src/effect/runtime-flags"
 import { Global } from "@opencode-ai/core/global"
 import { Permission } from "../../src/permission"
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { Plugin } from "../../src/plugin"
 import { Provider } from "../../src/provider/provider"
 import { Skill } from "../../src/skill"
@@ -28,7 +28,7 @@ const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
 const it = testEffect(agentLayer())
 
 // Helper to evaluate permission for a tool with wildcard pattern
-function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionLegacy.Action | undefined {
+function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionV1.Action | undefined {
   if (!agent) return undefined
   return Permission.evaluate(permission, "*", agent.permission).action
 }

+ 4 - 4
packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts

@@ -1,4 +1,4 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 /**
  * Reproducer for opencode issue #26514:
  *
@@ -61,7 +61,7 @@ it.instance("[#26514] subagent spawned from plan mode inherits read-only restric
     // session's `permission` field is empty (Plan Mode lives on the agent
     // ruleset, not the session). So we pass [] through as the parent
     // session permission, exactly like the actual code path.
-    const parentSessionPermission: PermissionLegacy.Ruleset = []
+    const parentSessionPermission: PermissionV1.Ruleset = []
 
     const subagentSessionPermission = deriveSubagentSessionPermission({
       parentSessionPermission,
@@ -89,7 +89,7 @@ it.instance("[#26514] explore subagent launched from plan mode also stays read-o
     expect(planAgent).toBeDefined()
     expect(explore).toBeDefined()
 
-    const parentSessionPermission: PermissionLegacy.Ruleset = []
+    const parentSessionPermission: PermissionV1.Ruleset = []
     const subagentSessionPermission = deriveSubagentSessionPermission({
       parentSessionPermission,
       parentAgent: planAgent,
@@ -114,7 +114,7 @@ it.instance(
       expect(planAgent).toBeDefined()
       expect(my).toBeDefined()
 
-      const parentSessionPermission: PermissionLegacy.Ruleset = []
+      const parentSessionPermission: PermissionV1.Ruleset = []
       const subagentSessionPermission = deriveSubagentSessionPermission({
         parentSessionPermission,
         parentAgent: planAgent,

+ 2 - 2
packages/opencode/test/cli/cmd/tui/aggregate-failures.test.ts

@@ -5,7 +5,7 @@
  */
 import { describe, expect, test } from "bun:test"
 import { aggregateFailures } from "@/cli/cmd/tui/context/aggregate-failures"
-import { ConfigError } from "@/config/error"
+import { ConfigErrorV1 } from "@opencode-ai/core/v1/config/error"
 
 describe("aggregateFailures", () => {
   test("returns null when every result is fulfilled", () => {
@@ -43,7 +43,7 @@ describe("aggregateFailures", () => {
   })
 
   test("formats structured config errors hidden inside SDK error causes", () => {
-    const configError = new ConfigError.InvalidError({
+    const configError = new ConfigErrorV1.InvalidError({
       path: "/tmp/opencode.json",
       issues: [{ message: "Expected object", path: ["provider", "anthropic", "options"] }],
     })

+ 6 - 6
packages/opencode/test/cli/github-action.test.ts

@@ -1,11 +1,11 @@
 import { test, expect, describe } from "bun:test"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
 import type { MessageV2 } from "../../src/session/message-v2"
 import { SessionID, MessageID, PartID } from "../../src/session/schema"
 
 // Helper to create minimal valid parts
-function createTextPart(text: string): SessionLegacy.Part {
+function createTextPart(text: string): SessionV1.Part {
   return {
     id: PartID.ascending(),
     sessionID: SessionID.make("ses_test"),
@@ -15,7 +15,7 @@ function createTextPart(text: string): SessionLegacy.Part {
   }
 }
 
-function createReasoningPart(text: string): SessionLegacy.Part {
+function createReasoningPart(text: string): SessionV1.Part {
   return {
     id: PartID.ascending(),
     sessionID: SessionID.make("ses_test"),
@@ -30,7 +30,7 @@ function createToolPart(
   tool: string,
   title: string,
   status: "completed" | "running" = "completed",
-): SessionLegacy.Part {
+): SessionV1.Part {
   if (status === "completed") {
     return {
       id: PartID.ascending(),
@@ -64,7 +64,7 @@ function createToolPart(
   }
 }
 
-function createStepStartPart(): SessionLegacy.Part {
+function createStepStartPart(): SessionV1.Part {
   return {
     id: PartID.ascending(),
     sessionID: SessionID.make("ses_test"),
@@ -73,7 +73,7 @@ function createStepStartPart(): SessionLegacy.Part {
   }
 }
 
-function createStepFinishPart(): SessionLegacy.Part {
+function createStepFinishPart(): SessionV1.Part {
   return {
     id: PartID.ascending(),
     sessionID: SessionID.make("ses_test"),

+ 13 - 11
packages/opencode/test/config/config.test.ts

@@ -1,4 +1,5 @@
 import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { Effect, Exit, Layer, Option } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
 import { NodeFileSystem, NodePath } from "@effect/platform-node"
@@ -34,6 +35,7 @@ import { Global } from "@opencode-ai/core/global"
 import { ProjectV2 } from "@opencode-ai/core/project"
 import { Filesystem } from "@/util/filesystem"
 import { ConfigPlugin } from "@/config/plugin"
+import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
 import { AccountTest } from "../fake/account"
 import { AuthTest } from "../fake/auth"
 import { NpmTest } from "../fake/npm"
@@ -360,7 +362,7 @@ it.instance("updates config and preserves empty shell sentinel", () =>
       "config.json",
     )
 
-    yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(Config.Info, { shell: "" }, "test:config")))
+    yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(ConfigV1.Info, { shell: "" }, "test:config")))
 
     const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
     expect(writtenConfig).toMatchObject({ shell: "" })
@@ -385,7 +387,7 @@ it.effect("updates global config and omits empty shell key in jsonc", () =>
 
       const file = path.join(dir, "opencode.jsonc")
       const writtenConfig = yield* FSUtil.use.readFileString(file)
-      const parsed = ConfigParse.schema(Config.Info, ConfigParse.jsonc(writtenConfig, file), file)
+      const parsed = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(writtenConfig, file), file)
       expect(writtenConfig).not.toContain('"shell"')
       expect(parsed.shell).toBeUndefined()
       expect(parsed.model).toBe("test/model")
@@ -870,7 +872,7 @@ it.instance("updates config and writes to file", () =>
   Effect.gen(function* () {
     const test = yield* TestInstance
     yield* Config.Service.use((svc) =>
-      svc.update(ConfigParse.schema(Config.Info, { model: "updated/model" }, "test:config")),
+      svc.update(ConfigParse.schema(ConfigV1.Info, { model: "updated/model" }, "test:config")),
     )
 
     const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
@@ -1284,7 +1286,7 @@ it.instance("permission config preserves user key order", () =>
 
 test("config parser preserves permission order while rejecting unknown top-level keys", () => {
   const config = ConfigParse.schema(
-    Config.Info,
+    ConfigV1.Info,
     {
       permission: {
         bash: "allow",
@@ -1297,7 +1299,7 @@ test("config parser preserves permission order while rejecting unknown top-level
 
   expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"])
   try {
-    ConfigParse.schema(Config.Info, { invalid_field: true }, "test")
+    ConfigParse.schema(ConfigV1.Info, { invalid_field: true }, "test")
     throw new Error("expected config parse to fail")
   } catch (err) {
     const error = err as { data?: { issues?: Array<{ code?: string; keys?: string[]; path?: string[] }> } }
@@ -1684,7 +1686,7 @@ describe("resolvePluginSpec", () => {
 })
 
 describe("deduplicatePluginOrigins", () => {
-  const dedupe = (plugins: ConfigPlugin.Spec[]) =>
+  const dedupe = (plugins: ConfigPluginV1.Spec[]) =>
     ConfigPlugin.deduplicatePluginOrigins(
       plugins.map((spec) => ({
         spec,
@@ -1883,7 +1885,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
 
 test("parseManagedPlist strips MDM metadata keys", async () => {
   const config = ConfigParse.schema(
-    Config.Info,
+    ConfigV1.Info,
     ConfigParse.jsonc(
       await ConfigManaged.parseManagedPlist(
         JSON.stringify({
@@ -1911,7 +1913,7 @@ test("parseManagedPlist strips MDM metadata keys", async () => {
 
 test("parseManagedPlist parses server settings", async () => {
   const config = ConfigParse.schema(
-    Config.Info,
+    ConfigV1.Info,
     ConfigParse.jsonc(
       await ConfigManaged.parseManagedPlist(
         JSON.stringify({
@@ -1931,7 +1933,7 @@ test("parseManagedPlist parses server settings", async () => {
 
 test("parseManagedPlist parses permission rules", async () => {
   const config = ConfigParse.schema(
-    Config.Info,
+    ConfigV1.Info,
     ConfigParse.jsonc(
       await ConfigManaged.parseManagedPlist(
         JSON.stringify({
@@ -1961,7 +1963,7 @@ test("parseManagedPlist parses permission rules", async () => {
 
 test("parseManagedPlist parses enabled_providers", async () => {
   const config = ConfigParse.schema(
-    Config.Info,
+    ConfigV1.Info,
     ConfigParse.jsonc(
       await ConfigManaged.parseManagedPlist(
         JSON.stringify({
@@ -1978,7 +1980,7 @@ test("parseManagedPlist parses enabled_providers", async () => {
 
 test("parseManagedPlist handles empty config", async () => {
   const config = ConfigParse.schema(
-    Config.Info,
+    ConfigV1.Info,
     ConfigParse.jsonc(
       await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://opencode.ai/config.json" })),
       "test:mobileconfig",

+ 3 - 3
packages/opencode/test/config/lsp.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect, test } from "bun:test"
 import { Schema } from "effect"
-import { ConfigLSP } from "../../src/config/lsp"
+import { ConfigLSPV1 } from "@opencode-ai/core/v1/config/lsp"
 
 // The LSP config refinement enforces: any custom (non-builtin) LSP server
 // entry must declare an `extensions` array so the client knows which files
@@ -8,8 +8,8 @@ import { ConfigLSP } from "../../src/config/lsp"
 // entries are exempt.
 //
 // `typescript` is a builtin server id (see src/lsp/server.ts).
-describe("ConfigLSP.Info refinement", () => {
-  const decodeEffect = Schema.decodeUnknownSync(ConfigLSP.Info)
+describe("ConfigLSPV1.Info refinement", () => {
+  const decodeEffect = Schema.decodeUnknownSync(ConfigLSPV1.Info)
 
   describe("accepted inputs", () => {
     test("true and false pass (top-level toggle)", () => {

+ 1 - 1
packages/opencode/test/fixture/config.ts

@@ -1,5 +1,5 @@
 import { Config } from "@/config/config"
-import { emptyConsoleState } from "@/config/console-state"
+import { emptyConsoleState } from "@opencode-ai/core/v1/config/console-state"
 import { Effect, Layer } from "effect"
 
 export function make(overrides: Partial<Config.Interface> = {}) {

+ 6 - 5
packages/opencode/test/fixture/fixture.ts

@@ -1,4 +1,5 @@
 import { $ } from "bun"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import * as fs from "fs/promises"
 import os from "os"
 import path from "path"
@@ -72,7 +73,7 @@ async function stop(dir: string) {
 
 type TmpDirOptions<T> = {
   git?: boolean
-  config?: Partial<Config.Info>
+  config?: Partial<ConfigV1.Info>
   init?: (dir: string) => Promise<T>
   dispose?: (dir: string) => Promise<T>
 }
@@ -116,7 +117,7 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
 /** Effectful scoped tmpdir. Cleaned up when the scope closes. Make sure these stay in sync */
 export function tmpdirScoped<E = never, R = never>(options?: {
   git?: boolean
-  config?: Partial<Config.Info> | (() => Partial<Config.Info>)
+  config?: Partial<ConfigV1.Info> | (() => Partial<ConfigV1.Info>)
   init?: (directory: string) => Effect.Effect<void, E, R>
 }) {
   return Effect.gen(function* () {
@@ -177,7 +178,7 @@ export const disposeAllInstancesEffect = InstanceStore.Service.use((store) => st
 
 export function provideTmpdirInstance<A, E, R>(
   self: (path: string) => Effect.Effect<A, E, R>,
-  options?: { git?: boolean; config?: Partial<Config.Info> | (() => Partial<Config.Info>) },
+  options?: { git?: boolean; config?: Partial<ConfigV1.Info> | (() => Partial<ConfigV1.Info>) },
 ) {
   return Effect.gen(function* () {
     const path = yield* tmpdirScoped(options)
@@ -196,7 +197,7 @@ export const requireInstance = Effect.gen(function* () {
 export const withTmpdirInstance =
   <E2 = never, R2 = never>(options?: {
     git?: boolean
-    config?: Partial<Config.Info> | (() => Partial<Config.Info>)
+    config?: Partial<ConfigV1.Info> | (() => Partial<ConfigV1.Info>)
     init?: (directory: string) => Effect.Effect<void, E2, R2>
   }) =>
   <A, E, R>(self: Effect.Effect<A, E, R>) =>
@@ -207,7 +208,7 @@ export const withTmpdirInstance =
 
 export function provideTmpdirServer<A, E, R>(
   self: (input: { dir: string; llm: TestLLMServer["Service"] }) => Effect.Effect<A, E, R>,
-  options?: { git?: boolean; config?: (url: string) => Partial<Config.Info> },
+  options?: { git?: boolean; config?: (url: string) => Partial<ConfigV1.Info> },
 ): Effect.Effect<
   A,
   E | PlatformError.PlatformError,

+ 2 - 1
packages/opencode/test/lib/effect.ts

@@ -1,4 +1,5 @@
 import { test, type TestOptions } from "bun:test"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { Cause, Duration, Effect, Exit, Layer } from "effect"
 import * as Scope from "effect/Scope"
 import * as TestClock from "effect/testing/TestClock"
@@ -11,7 +12,7 @@ import { InstanceStore } from "@/project/instance-store"
 type Body<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
 type InstanceOptions<E, R> = {
   git?: boolean
-  config?: Partial<Config.Info> | (() => Partial<Config.Info>)
+  config?: Partial<ConfigV1.Info> | (() => Partial<ConfigV1.Info>)
   init?: (directory: string) => Effect.Effect<void, E, R>
 }
 

+ 3 - 3
packages/opencode/test/permission-task.test.ts

@@ -1,4 +1,4 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { describe, test, expect } from "bun:test"
 import { Effect } from "effect"
 import { Permission } from "../src/permission"
@@ -10,7 +10,7 @@ const it = testEffect(Config.defaultLayer)
 const load = Config.use.get()
 
 describe("Permission.evaluate for permission.task", () => {
-  const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionLegacy.Ruleset =>
+  const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionV1.Ruleset =>
     Object.entries(rules).map(([pattern, action]) => ({
       permission: "task",
       pattern,
@@ -76,7 +76,7 @@ describe("Permission.disabled for task tool", () => {
   // Note: The `disabled` function checks if a TOOL should be completely removed from the tool list.
   // It only disables a tool when there's a rule with `pattern: "*"` and `action: "deny"`.
   // It does NOT evaluate complex subagent patterns - those are handled at runtime by `evaluate`.
-  const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionLegacy.Ruleset =>
+  const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionV1.Ruleset =>
     Object.entries(rules).map(([pattern, action]) => ({
       permission: "task",
       pattern,

+ 51 - 51
packages/opencode/test/permission/next.test.ts

@@ -1,4 +1,4 @@
-import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
+import { PermissionV1 } from "@opencode-ai/core/v1/permission"
 import { test, expect } from "bun:test"
 import os from "os"
 import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
@@ -261,8 +261,8 @@ test("merge - preserves rule order", () => {
 })
 
 test("merge - config permission overrides default ask", () => {
-  const defaults: PermissionLegacy.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
-  const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
+  const defaults: PermissionV1.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
+  const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
   const merged = Permission.merge(defaults, config)
 
   expect(Permission.evaluate("bash", "ls", merged).action).toBe("allow")
@@ -270,8 +270,8 @@ test("merge - config permission overrides default ask", () => {
 })
 
 test("merge - config ask overrides default allow", () => {
-  const defaults: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
-  const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
+  const defaults: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
+  const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
   const merged = Permission.merge(defaults, config)
 
   expect(Permission.evaluate("bash", "ls", merged).action).toBe("ask")
@@ -443,8 +443,8 @@ test("evaluate - later wildcard permission can override earlier specific permiss
 })
 
 test("evaluate - merges multiple rulesets", () => {
-  const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
-  const approved: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
+  const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
+  const approved: PermissionV1.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
   const result = Permission.evaluate("bash", "rm", config, approved)
   expect(result.action).toBe("deny")
 })
@@ -588,7 +588,7 @@ it.instance(
           ruleset: [{ permission: "bash", pattern: "*", action: "deny" }],
         }),
       )
-      expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
+      expect(err).toBeInstanceOf(PermissionV1.DeniedError)
     }),
   { git: true },
 )
@@ -655,10 +655,10 @@ it.instance(
   () =>
     Effect.gen(function* () {
       const events = yield* EventV2Bridge.Service
-      const seen = yield* Deferred.make<PermissionLegacy.Request>()
+      const seen = yield* Deferred.make<PermissionV1.Request>()
       const unsub = yield* events.listen((event) => {
         if (event.type === Permission.Event.Asked.type)
-          Deferred.doneUnsafe(seen, Effect.succeed(event.data as PermissionLegacy.Request))
+          Deferred.doneUnsafe(seen, Effect.succeed(event.data as PermissionV1.Request))
         return Effect.void
       })
       yield* Effect.addFinalizer(() => unsub)
@@ -703,7 +703,7 @@ it.instance(
   () =>
     Effect.gen(function* () {
       const fiber = yield* ask({
-        id: PermissionLegacy.ID.make("per_test1"),
+        id: PermissionV1.ID.make("per_test1"),
         sessionID: SessionID.make("session_test"),
         permission: "bash",
         patterns: ["ls"],
@@ -713,7 +713,7 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       yield* waitForPending(1)
-      yield* reply({ requestID: PermissionLegacy.ID.make("per_test1"), reply: "once" })
+      yield* reply({ requestID: PermissionV1.ID.make("per_test1"), reply: "once" })
       yield* Fiber.join(fiber)
     }),
   { git: true },
@@ -724,7 +724,7 @@ it.instance(
   () =>
     Effect.gen(function* () {
       const fiber = yield* ask({
-        id: PermissionLegacy.ID.make("per_test2"),
+        id: PermissionV1.ID.make("per_test2"),
         sessionID: SessionID.make("session_test"),
         permission: "bash",
         patterns: ["ls"],
@@ -734,11 +734,11 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       yield* waitForPending(1)
-      yield* reply({ requestID: PermissionLegacy.ID.make("per_test2"), reply: "reject" })
+      yield* reply({ requestID: PermissionV1.ID.make("per_test2"), reply: "reject" })
 
       const exit = yield* Fiber.await(fiber)
       expect(Exit.isFailure(exit)).toBe(true)
-      if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
+      if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionV1.RejectedError)
     }),
   { git: true },
 )
@@ -748,7 +748,7 @@ it.instance(
   () =>
     Effect.gen(function* () {
       const fiber = yield* ask({
-        id: PermissionLegacy.ID.make("per_test2b"),
+        id: PermissionV1.ID.make("per_test2b"),
         sessionID: SessionID.make("session_test"),
         permission: "bash",
         patterns: ["ls"],
@@ -759,7 +759,7 @@ it.instance(
 
       yield* waitForPending(1)
       yield* reply({
-        requestID: PermissionLegacy.ID.make("per_test2b"),
+        requestID: PermissionV1.ID.make("per_test2b"),
         reply: "reject",
         message: "Use a safer command",
       })
@@ -768,7 +768,7 @@ it.instance(
       expect(Exit.isFailure(exit)).toBe(true)
       if (Exit.isFailure(exit)) {
         const err = Cause.squash(exit.cause)
-        expect(err).toBeInstanceOf(PermissionLegacy.CorrectedError)
+        expect(err).toBeInstanceOf(PermissionV1.CorrectedError)
         expect(String(err)).toContain("Use a safer command")
       }
     }),
@@ -780,7 +780,7 @@ it.instance(
   () =>
     Effect.gen(function* () {
       const fiber = yield* ask({
-        id: PermissionLegacy.ID.make("per_test3"),
+        id: PermissionV1.ID.make("per_test3"),
         sessionID: SessionID.make("session_test"),
         permission: "bash",
         patterns: ["ls"],
@@ -790,7 +790,7 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       yield* waitForPending(1)
-      yield* reply({ requestID: PermissionLegacy.ID.make("per_test3"), reply: "always" })
+      yield* reply({ requestID: PermissionV1.ID.make("per_test3"), reply: "always" })
       yield* Fiber.join(fiber)
 
       const result = yield* ask({
@@ -811,7 +811,7 @@ it.instance(
   () =>
     Effect.gen(function* () {
       const a = yield* ask({
-        id: PermissionLegacy.ID.make("per_test4a"),
+        id: PermissionV1.ID.make("per_test4a"),
         sessionID: SessionID.make("session_same"),
         permission: "bash",
         patterns: ["ls"],
@@ -821,7 +821,7 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       const b = yield* ask({
-        id: PermissionLegacy.ID.make("per_test4b"),
+        id: PermissionV1.ID.make("per_test4b"),
         sessionID: SessionID.make("session_same"),
         permission: "edit",
         patterns: ["foo.ts"],
@@ -831,13 +831,13 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       yield* waitForPending(2)
-      yield* reply({ requestID: PermissionLegacy.ID.make("per_test4a"), reply: "reject" })
+      yield* reply({ requestID: PermissionV1.ID.make("per_test4a"), reply: "reject" })
 
       const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
       expect(Exit.isFailure(ea)).toBe(true)
       expect(Exit.isFailure(eb)).toBe(true)
-      if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
-      if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
+      if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(PermissionV1.RejectedError)
+      if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(PermissionV1.RejectedError)
     }),
   { git: true },
 )
@@ -847,7 +847,7 @@ it.instance(
   () =>
     Effect.gen(function* () {
       const a = yield* ask({
-        id: PermissionLegacy.ID.make("per_test5a"),
+        id: PermissionV1.ID.make("per_test5a"),
         sessionID: SessionID.make("session_same"),
         permission: "bash",
         patterns: ["ls"],
@@ -857,7 +857,7 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       const b = yield* ask({
-        id: PermissionLegacy.ID.make("per_test5b"),
+        id: PermissionV1.ID.make("per_test5b"),
         sessionID: SessionID.make("session_same"),
         permission: "bash",
         patterns: ["ls"],
@@ -867,7 +867,7 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       yield* waitForPending(2)
-      yield* reply({ requestID: PermissionLegacy.ID.make("per_test5a"), reply: "always" })
+      yield* reply({ requestID: PermissionV1.ID.make("per_test5a"), reply: "always" })
 
       yield* Fiber.join(a)
       yield* Fiber.join(b)
@@ -881,7 +881,7 @@ it.instance(
   () =>
     Effect.gen(function* () {
       const a = yield* ask({
-        id: PermissionLegacy.ID.make("per_test6a"),
+        id: PermissionV1.ID.make("per_test6a"),
         sessionID: SessionID.make("session_a"),
         permission: "bash",
         patterns: ["ls"],
@@ -891,7 +891,7 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       const b = yield* ask({
-        id: PermissionLegacy.ID.make("per_test6b"),
+        id: PermissionV1.ID.make("per_test6b"),
         sessionID: SessionID.make("session_b"),
         permission: "bash",
         patterns: ["ls"],
@@ -901,10 +901,10 @@ it.instance(
       }).pipe(Effect.forkScoped)
 
       yield* waitForPending(2)
-      yield* reply({ requestID: PermissionLegacy.ID.make("per_test6a"), reply: "always" })
+      yield* reply({ requestID: PermissionV1.ID.make("per_test6a"), reply: "always" })
 
       yield* Fiber.join(a)
-      expect((yield* list()).map((item) => item.id)).toEqual([PermissionLegacy.ID.make("per_test6b")])
+      expect((yield* list()).map((item) => item.id)).toEqual([PermissionV1.ID.make("per_test6b")])
 
       yield* rejectAll()
       yield* Fiber.await(b)
@@ -919,12 +919,12 @@ it.instance(
       const events = yield* EventV2Bridge.Service
       const seen = yield* Deferred.make<{
         sessionID: SessionID
-        requestID: PermissionLegacy.ID
-        reply: PermissionLegacy.Reply
+        requestID: PermissionV1.ID
+        reply: PermissionV1.Reply
       }>()
 
       const fiber = yield* ask({
-        id: PermissionLegacy.ID.make("per_test7"),
+        id: PermissionV1.ID.make("per_test7"),
         sessionID: SessionID.make("session_test"),
         permission: "bash",
         patterns: ["ls"],
@@ -940,14 +940,14 @@ it.instance(
           Deferred.doneUnsafe(
             seen,
             Effect.succeed(
-              event.data as { sessionID: SessionID; requestID: PermissionLegacy.ID; reply: PermissionLegacy.Reply },
+              event.data as { sessionID: SessionID; requestID: PermissionV1.ID; reply: PermissionV1.Reply },
             ),
           )
         return Effect.void
       })
       yield* Effect.addFinalizer(() => unsub)
 
-      yield* reply({ requestID: PermissionLegacy.ID.make("per_test7"), reply: "once" })
+      yield* reply({ requestID: PermissionV1.ID.make("per_test7"), reply: "once" })
       yield* Fiber.join(fiber)
       expect(
         yield* Deferred.await(seen).pipe(
@@ -958,7 +958,7 @@ it.instance(
         ),
       ).toEqual({
         sessionID: SessionID.make("session_test"),
-        requestID: PermissionLegacy.ID.make("per_test7"),
+        requestID: PermissionV1.ID.make("per_test7"),
         reply: "once",
       })
     }),
@@ -975,7 +975,7 @@ it.live("permission requests stay isolated by directory", () =>
       .provide(
         { directory: one },
         ask({
-          id: PermissionLegacy.ID.make("per_dir_a"),
+          id: PermissionV1.ID.make("per_dir_a"),
           sessionID: SessionID.make("session_dir_a"),
           permission: "bash",
           patterns: ["ls"],
@@ -990,7 +990,7 @@ it.live("permission requests stay isolated by directory", () =>
       .provide(
         { directory: two },
         ask({
-          id: PermissionLegacy.ID.make("per_dir_b"),
+          id: PermissionV1.ID.make("per_dir_b"),
           sessionID: SessionID.make("session_dir_b"),
           permission: "bash",
           patterns: ["pwd"],
@@ -1006,8 +1006,8 @@ it.live("permission requests stay isolated by directory", () =>
 
     expect(onePending).toHaveLength(1)
     expect(twoPending).toHaveLength(1)
-    expect(onePending[0].id).toBe(PermissionLegacy.ID.make("per_dir_a"))
-    expect(twoPending[0].id).toBe(PermissionLegacy.ID.make("per_dir_b"))
+    expect(onePending[0].id).toBe(PermissionV1.ID.make("per_dir_a"))
+    expect(twoPending[0].id).toBe(PermissionV1.ID.make("per_dir_b"))
 
     yield* store.provide({ directory: one }, reply({ requestID: onePending[0].id, reply: "reject" }))
     yield* store.provide({ directory: two }, reply({ requestID: twoPending[0].id, reply: "reject" }))
@@ -1024,7 +1024,7 @@ it.instance(
       const test = yield* TestInstance
       const store = yield* InstanceStore.Service
       const fiber = yield* ask({
-        id: PermissionLegacy.ID.make("per_dispose"),
+        id: PermissionV1.ID.make("per_dispose"),
         sessionID: SessionID.make("session_dispose"),
         permission: "bash",
         patterns: ["ls"],
@@ -1039,7 +1039,7 @@ it.instance(
 
       const exit = yield* Fiber.await(fiber)
       expect(Exit.isFailure(exit)).toBe(true)
-      if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
+      if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionV1.RejectedError)
     }),
   { git: true },
 )
@@ -1051,7 +1051,7 @@ it.instance(
       const test = yield* TestInstance
       const store = yield* InstanceStore.Service
       const fiber = yield* ask({
-        id: PermissionLegacy.ID.make("per_reload"),
+        id: PermissionV1.ID.make("per_reload"),
         sessionID: SessionID.make("session_reload"),
         permission: "bash",
         patterns: ["ls"],
@@ -1065,7 +1065,7 @@ it.instance(
 
       const exit = yield* Fiber.await(fiber)
       expect(Exit.isFailure(exit)).toBe(true)
-      if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
+      if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionV1.RejectedError)
     }),
   { git: true },
 )
@@ -1074,7 +1074,7 @@ it.instance(
   "reply - fails for unknown requestID",
   () =>
     Effect.gen(function* () {
-      const exit = yield* reply({ requestID: PermissionLegacy.ID.make("per_unknown"), reply: "once" }).pipe(Effect.exit)
+      const exit = yield* reply({ requestID: PermissionV1.ID.make("per_unknown"), reply: "once" }).pipe(Effect.exit)
       expect(Exit.isFailure(exit)).toBe(true)
       if (Exit.isFailure(exit)) {
         expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "Permission.NotFoundError", requestID: "per_unknown" })
@@ -1101,7 +1101,7 @@ it.instance(
           ],
         }),
       )
-      expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
+      expect(err).toBeInstanceOf(PermissionV1.DeniedError)
     }),
   { git: true },
 )
@@ -1141,7 +1141,7 @@ it.instance(
         }),
       )
 
-      expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
+      expect(err).toBeInstanceOf(PermissionV1.DeniedError)
       expect(yield* list()).toHaveLength(0)
     }),
   { git: true },
@@ -1155,7 +1155,7 @@ it.instance(
       const store = yield* InstanceStore.Service
 
       const fiber = yield* ask({
-        id: PermissionLegacy.ID.make("per_reload"),
+        id: PermissionV1.ID.make("per_reload"),
         sessionID: SessionID.make("session_reload"),
         permission: "bash",
         patterns: ["ls"],
@@ -1170,7 +1170,7 @@ it.instance(
 
       const exit = yield* Fiber.await(fiber)
       expect(Exit.isFailure(exit)).toBe(true)
-      if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
+      if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionV1.RejectedError)
     }),
   { git: true },
 )

+ 2 - 2
packages/opencode/test/provider/model-status.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect, test } from "bun:test"
 import { Schema } from "effect"
-import { ConfigProvider } from "@/config/provider"
+import { ConfigProviderV1 } from "@opencode-ai/core/v1/config/provider"
 import { CatalogModelStatus, ModelStatus } from "@/provider/model-status"
 import { ModelsDev } from "@opencode-ai/core/models-dev"
 import { Provider } from "@/provider/provider"
@@ -13,7 +13,7 @@ describe("provider model status schemas", () => {
   })
 
   test("accepts active status across public provider schemas", () => {
-    expect(Schema.decodeUnknownSync(ConfigProvider.Model)({ status: "active" }).status).toBe("active")
+    expect(Schema.decodeUnknownSync(ConfigProviderV1.Model)({ status: "active" }).status).toBe("active")
     expect(
       Schema.decodeUnknownSync(ModelsDev.Model)({
         id: "test-model",

+ 2 - 2
packages/opencode/test/server/httpapi-error-middleware.test.ts

@@ -1,7 +1,7 @@
 import { NodeHttpServer, NodeServices } from "@effect/platform-node"
 import { NamedError } from "@opencode-ai/core/util/error"
 import { describe, expect } from "bun:test"
-import { ConfigError } from "../../src/config/error"
+import { ConfigErrorV1 } from "@opencode-ai/core/v1/config/error"
 import { Effect, Layer } from "effect"
 import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http"
 import { errorLayer } from "../../src/server/routes/instance/httpapi/middleware/error"
@@ -55,7 +55,7 @@ describe("HttpApi error middleware", () => {
 
   it.live("does not expose config defects from generic middleware", () =>
     Effect.gen(function* () {
-      const configError = new ConfigError.InvalidError({
+      const configError = new ConfigErrorV1.InvalidError({
         path: "/tmp/opencode.json",
         issues: [{ message: "Expected object", path: ["provider", "anthropic", "options"] }],
       })

+ 6 - 5
packages/opencode/test/server/httpapi-exercise/runner.ts

@@ -1,5 +1,6 @@
 import { Flag } from "@opencode-ai/core/flag/flag"
-import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
+import { SessionV1 } from "@opencode-ai/core/v1/session"
 import { Cause, Duration, Effect, Layer, Scope } from "effect"
 import { TestLLMServer } from "../../lib/llm-server"
 import type { Config } from "../../../src/config/config"
@@ -144,7 +145,7 @@ function withContext<A, E>(
             }),
           message: (sessionID, input) =>
             Effect.gen(function* () {
-              const info: SessionLegacy.User = {
+              const info: SessionV1.User = {
                 id: MessageID.ascending(),
                 sessionID,
                 role: "user",
@@ -155,7 +156,7 @@ function withContext<A, E>(
                   modelID: ProviderV2.ModelID.make("test"),
                 },
               }
-              const part: SessionLegacy.TextPart = {
+              const part: SessionV1.TextPart = {
                 id: PartID.ascending(),
                 sessionID,
                 messageID: info.id,
@@ -205,7 +206,7 @@ function trace(options: Options, scenario: ActiveScenario, phase: string) {
 function projectOptions(
   project: ProjectOptions,
   llmUrl: string | undefined,
-): { git?: boolean; config?: Partial<Config.Info> } {
+): { git?: boolean; config?: Partial<ConfigV1.Info> } {
   if (!project.llm || !llmUrl) return { git: project.git, config: project.config }
   const fake = fakeLlmConfig(llmUrl)
   return {
@@ -221,7 +222,7 @@ function projectOptions(
   }
 }
 
-function fakeLlmConfig(url: string): Partial<Config.Info> {
+function fakeLlmConfig(url: string): Partial<ConfigV1.Info> {
   return {
     model: "test/test-model",
     small_model: "test/test-model",

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio