Przeglądaj źródła

Remove Zod from named errors (#26982)

Kit Langton 3 miesięcy temu
rodzic
commit
28f38fc871

+ 31 - 22
packages/core/src/util/error.ts

@@ -1,8 +1,8 @@
-import z from "zod"
+import { Schema } from "effect"
 
 export abstract class NamedError extends Error {
-  abstract schema(): z.core.$ZodType
-  abstract toObject(): { name: string; data: any }
+  abstract schema(): Schema.Top
+  abstract toObject(): { name: string; data: unknown }
 
   static hasName(error: unknown, name: string): boolean {
     return (
@@ -10,30 +10,42 @@ export abstract class NamedError extends Error {
     )
   }
 
-  static create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data) {
-    const schema = z
-      .object({
-        name: z.literal(name),
-        data,
-      })
-      .meta({
-        ref: name,
-      })
+  static create<Name extends string, Fields extends Schema.Struct.Fields>(
+    name: Name,
+    fields: Fields,
+  ): ReturnType<typeof NamedError.createSchemaClass<Name, Schema.Struct<Fields>>>
+  static create<Name extends string, DataSchema extends Schema.Top>(
+    name: Name,
+    data: DataSchema,
+  ): ReturnType<typeof NamedError.createSchemaClass<Name, DataSchema>>
+  static create<Name extends string>(name: Name, data: Schema.Top | Schema.Struct.Fields) {
+    return NamedError.createSchemaClass(name, Schema.isSchema(data) ? data : Schema.Struct(data))
+  }
+
+  private static createSchemaClass<Name extends string, DataSchema extends Schema.Top>(name: Name, data: DataSchema) {
+    const schema = Schema.Struct({
+      name: Schema.Literal(name),
+      data,
+    }).annotate({ identifier: name })
+    type Data = Schema.Schema.Type<DataSchema>
+
     const result = class extends NamedError {
       public static readonly Schema = schema
+      public static readonly EffectSchema = schema
+      public static readonly tag = name
 
-      public override readonly name = name as Name
+      public override readonly name = name
 
       constructor(
-        public readonly data: z.input<Data>,
+        public readonly data: Data,
         options?: ErrorOptions,
       ) {
         super(name, options)
         this.name = name
       }
 
-      static isInstance(input: any): input is InstanceType<typeof result> {
-        return typeof input === "object" && "name" in input && input.name === name
+      static isInstance(input: unknown): input is InstanceType<typeof result> {
+        return NamedError.hasName(input, name)
       }
 
       schema() {
@@ -51,10 +63,7 @@ export abstract class NamedError extends Error {
     return result
   }
 
-  public static readonly Unknown = NamedError.create(
-    "UnknownError",
-    z.object({
-      message: z.string(),
-    }),
-  )
+  public static readonly Unknown = NamedError.create("UnknownError", {
+    message: Schema.String,
+  })
 }

+ 22 - 8
packages/enterprise/src/routes/share/[shareID].tsx

@@ -15,7 +15,6 @@ import { Binary } from "@opencode-ai/core/util/binary"
 import { NamedError } from "@opencode-ai/core/util/error"
 import { DateTime } from "luxon"
 import { createStore } from "solid-js/store"
-import z from "zod"
 import NotFound from "../[...404]"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { MessageNav } from "@opencode-ai/ui/message-nav"
@@ -33,13 +32,28 @@ const ClientOnlyWorkerPoolProvider = clientOnly(() =>
   })),
 )
 
-const SessionDataMissingError = NamedError.create(
-  "SessionDataMissingError",
-  z.object({
-    sessionID: z.string(),
-    message: z.string().optional(),
-  }),
-)
+class SessionDataMissingError extends NamedError {
+  public override readonly name = "SessionDataMissingError"
+
+  constructor(
+    public readonly data: { sessionID: string; message?: string },
+    options?: ErrorOptions,
+  ) {
+    super("SessionDataMissingError", options)
+  }
+
+  static isInstance(input: unknown): input is SessionDataMissingError {
+    return NamedError.hasName(input, "SessionDataMissingError")
+  }
+
+  schema(): never {
+    throw new Error("SessionDataMissingError does not expose a schema")
+  }
+
+  toObject() {
+    return { name: this.name, data: this.data }
+  }
+}
 
 const getData = query(async (shareID) => {
   "use server"

+ 2 - 2
packages/opencode/src/cli/ui.ts

@@ -1,6 +1,6 @@
-import z from "zod"
 import { EOL } from "os"
 import { NamedError } from "@opencode-ai/core/util/error"
+import { Schema } from "effect"
 import { logo as glyphs } from "./logo"
 
 const wordmark = [
@@ -10,7 +10,7 @@ const wordmark = [
   `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀  ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
 ]
 
-export const CancelledError = NamedError.create("UICancelledError", z.void())
+export const CancelledError = NamedError.create("UICancelledError", Schema.optional(Schema.Void))
 
 export const Style = {
   TEXT_HIGHLIGHT: "\x1b[96m",

+ 5 - 9
packages/opencode/src/config/config.ts

@@ -2,7 +2,6 @@ import * as Log from "@opencode-ai/core/util/log"
 import path from "path"
 import { pathToFileURL } from "url"
 import os from "os"
-import z from "zod"
 import { mergeDeep } from "remeda"
 import { Global } from "@opencode-ai/core/global"
 import fsNode from "fs/promises"
@@ -357,14 +356,11 @@ function writableGlobal(info: Info) {
   return next
 }
 
-export const ConfigDirectoryTypoError = NamedError.create(
-  "ConfigDirectoryTypoError",
-  z.object({
-    path: z.string(),
-    dir: z.string(),
-    suggestion: z.string(),
-  }),
-)
+export const ConfigDirectoryTypoError = NamedError.create("ConfigDirectoryTypoError", {
+  path: Schema.String,
+  dir: Schema.String,
+  suggestion: Schema.String,
+})
 
 export const layer = Layer.effect(
   Service,

+ 16 - 14
packages/opencode/src/config/error.ts

@@ -1,21 +1,23 @@
 export * as ConfigError from "./error"
 
-import z from "zod"
 import { NamedError } from "@opencode-ai/core/util/error"
+import { Schema } from "effect"
 
-export const JsonError = NamedError.create(
-  "ConfigJsonError",
-  z.object({
-    path: z.string(),
-    message: z.string().optional(),
+const Issue = Schema.StructWithRest(
+  Schema.Struct({
+    message: Schema.String,
+    path: Schema.Array(Schema.String),
   }),
+  [Schema.Record(Schema.String, Schema.Unknown)],
 )
 
-export const InvalidError = NamedError.create(
-  "ConfigInvalidError",
-  z.object({
-    path: z.string(),
-    issues: z.custom<z.core.$ZodIssue[]>().optional(),
-    message: z.string().optional(),
-  }),
-)
+export const JsonError = NamedError.create("ConfigJsonError", {
+  path: Schema.String,
+  message: Schema.optional(Schema.String),
+})
+
+export const InvalidError = NamedError.create("ConfigInvalidError", {
+  path: Schema.String,
+  issues: Schema.optional(Schema.Array(Issue)),
+  message: Schema.optional(Schema.String),
+})

+ 5 - 8
packages/opencode/src/config/markdown.ts

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

+ 7 - 4
packages/opencode/src/config/parse.ts

@@ -2,7 +2,6 @@ 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 z from "zod"
 import type { DeepMutable } from "@opencode-ai/core/schema"
 import { InvalidError, JsonError } from "./error"
 
@@ -48,7 +47,7 @@ export function schema<S extends EffectSchema.Decoder<unknown, never>>(
           keys: extra,
           path: [],
           message: `Unrecognized key${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}`,
-        } as z.core.$ZodIssue,
+        },
       ],
     })
   }
@@ -61,8 +60,12 @@ export function schema<S extends EffectSchema.Decoder<unknown, never>>(
     {
       path: source,
       issues: EffectSchema.isSchemaError(error)
-        ? (SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues as z.core.$ZodIssue[])
-        : ([{ code: "custom", message: String(error), path: [] }] as z.core.$ZodIssue[]),
+        ? SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues.map((issue) => ({
+            ...issue,
+            message: issue.message,
+            path: issue.path?.map(String) ?? [],
+          }))
+        : [{ message: String(error), path: [] }],
     },
     { cause: error },
   )

+ 4 - 8
packages/opencode/src/ide/index.ts

@@ -1,5 +1,4 @@
 import { BusEvent } from "@/bus/bus-event"
-import z from "zod"
 import { Schema } from "effect"
 import { NamedError } from "@opencode-ai/core/util/error"
 import * as Log from "@opencode-ai/core/util/log"
@@ -24,14 +23,11 @@ export const Event = {
   ),
 }
 
-export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", z.object({}))
+export const AlreadyInstalledError = NamedError.create("AlreadyInstalledError", {})
 
-export const InstallFailedError = NamedError.create(
-  "InstallFailedError",
-  z.object({
-    stderr: z.string(),
-  }),
-)
+export const InstallFailedError = NamedError.create("InstallFailedError", {
+  stderr: Schema.String,
+})
 
 export function ide() {
   if (process.env["TERM_PROGRAM"] === "vscode") {

+ 11 - 7
packages/opencode/src/index.ts

@@ -39,6 +39,7 @@ import { PluginCommand } from "./cli/cmd/plug"
 import { Heap } from "./cli/heap"
 import { drizzle } from "drizzle-orm/bun-sqlite"
 import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
+import { isRecord } from "@/util/record"
 
 const processMetadata = ensureProcessMetadata("main")
 
@@ -203,13 +204,6 @@ try {
   }
 } catch (e) {
   let data: Record<string, any> = {}
-  if (e instanceof NamedError) {
-    const obj = e.toObject()
-    Object.assign(data, {
-      ...obj.data,
-    })
-  }
-
   if (e instanceof Error) {
     Object.assign(data, {
       name: e.name,
@@ -219,6 +213,16 @@ try {
     })
   }
 
+  if (e instanceof NamedError) {
+    const obj = e.toObject()
+    if (isRecord(obj.data)) {
+      for (const [key, value] of Object.entries(obj.data)) {
+        if (key === "name" || key === "stack" || key === "cause") continue
+        data[key] = value
+      }
+    }
+  }
+
   if (e instanceof ResolveMessage) {
     Object.assign(data, {
       name: e.name,

+ 3 - 7
packages/opencode/src/lsp/client.ts

@@ -7,7 +7,6 @@ import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types
 import * as Log from "@opencode-ai/core/util/log"
 import { Process } from "@/util/process"
 import { LANGUAGE_EXTENSIONS } from "./language"
-import z from "zod"
 import { Schema } from "effect"
 import type * as LSPServer from "./server"
 import { NamedError } from "@opencode-ai/core/util/error"
@@ -32,12 +31,9 @@ export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
 
 export type Diagnostic = VSCodeDiagnostic
 
-export const InitializeError = NamedError.create(
-  "LSPInitializeError",
-  z.object({
-    serverID: z.string(),
-  }),
-)
+export const InitializeError = NamedError.create("LSPInitializeError", {
+  serverID: Schema.String,
+})
 
 export const Event = {
   Diagnostics: BusEvent.define(

+ 3 - 6
packages/opencode/src/mcp/index.ts

@@ -68,12 +68,9 @@ export const BrowserOpenFailed = BusEvent.define(
   }),
 )
 
-export const Failed = NamedError.create(
-  "MCPFailed",
-  z.object({
-    name: z.string(),
-  }),
-)
+export const Failed = NamedError.create("MCPFailed", {
+  name: Schema.String,
+})
 
 type MCPClient = Client
 

+ 1 - 3
packages/opencode/src/session/message-v2.ts

@@ -382,9 +382,7 @@ export type Part =
 
 const AssistantErrorSchema = Schema.Union([
   AuthError.EffectSchema,
-  Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({
-    identifier: "UnknownError",
-  }),
+  NamedError.Unknown.EffectSchema,
   OutputLengthError.EffectSchema,
   AbortedError.EffectSchema,
   StructuredOutputError.EffectSchema,

+ 4 - 21
packages/opencode/src/session/message.ts

@@ -3,6 +3,7 @@ import { SessionID } from "./schema"
 import { ModelID, ProviderID } from "../provider/schema"
 import { NonNegativeInt } from "@opencode-ai/core/schema"
 import { namedSchemaError } from "@/util/named-schema-error"
+import { NamedError } from "@opencode-ai/core/util/error"
 
 export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
 export const AuthError = namedSchemaError("ProviderAuthError", {
@@ -10,26 +11,6 @@ export const AuthError = namedSchemaError("ProviderAuthError", {
   message: Schema.String,
 })
 
-const AuthErrorEffect = Schema.Struct({
-  name: Schema.Literal("ProviderAuthError"),
-  data: Schema.Struct({
-    providerID: Schema.String,
-    message: Schema.String,
-  }),
-})
-
-const OutputLengthErrorEffect = Schema.Struct({
-  name: Schema.Literal("MessageOutputLengthError"),
-  data: Schema.Struct({}),
-})
-
-const UnknownErrorEffect = Schema.Struct({
-  name: Schema.Literal("UnknownError"),
-  data: Schema.Struct({
-    message: Schema.String,
-  }),
-})
-
 export const ToolCall = Schema.Struct({
   state: Schema.Literal("call"),
   step: Schema.optional(NonNegativeInt),
@@ -124,7 +105,9 @@ export const Info = Schema.Struct({
       created: NonNegativeInt,
       completed: Schema.optional(NonNegativeInt),
     }),
-    error: Schema.optional(Schema.Union([AuthErrorEffect, UnknownErrorEffect, OutputLengthErrorEffect])),
+    error: Schema.optional(
+      Schema.Union([AuthError.EffectSchema, NamedError.Unknown.EffectSchema, OutputLengthError.EffectSchema]),
+    ),
     sessionID: SessionID,
     tool: Schema.Record(
       Schema.String,

+ 3 - 2
packages/opencode/src/session/retry.ts

@@ -2,6 +2,7 @@ import type { NamedError } from "@opencode-ai/core/util/error"
 import { Cause, Clock, Duration, Effect, Schedule } from "effect"
 import { MessageV2 } from "./message-v2"
 import { iife } from "@/util/iife"
+import { isRecord } from "@/util/record"
 
 export type Err = ReturnType<NamedError["toObject"]>
 
@@ -121,7 +122,7 @@ export function retryable(error: Err, provider: string) {
   }
 
   // Check for rate limit patterns in plain text error messages
-  const msg = error.data?.message
+  const msg = isRecord(error.data) ? error.data.message : undefined
   if (typeof msg === "string") {
     const lower = msg.toLowerCase()
     if (
@@ -133,7 +134,7 @@ export function retryable(error: Err, provider: string) {
     }
   }
 
-  const json = parseJSON(error.data?.message)
+  const json = parseJSON(msg)
   if (!json || typeof json !== "object") return undefined
   const code = typeof json.code === "string" ? json.code : ""
 

+ 32 - 23
packages/opencode/src/skill/index.ts

@@ -1,6 +1,5 @@
 import path from "path"
 import { pathToFileURL } from "url"
-import z from "zod"
 import { Effect, Layer, Context, Schema } from "effect"
 import { NamedError } from "@opencode-ai/core/util/error"
 import type { Agent } from "@/agent/agent"
@@ -16,6 +15,7 @@ import { Glob } from "@opencode-ai/core/util/glob"
 import * as Log from "@opencode-ai/core/util/log"
 import { Discovery } from "./discovery"
 import CUSTOMIZE_OPENCODE_SKILL_BODY from "./prompt/customize-opencode.md" with { type: "text" }
+import { isRecord } from "@/util/record"
 
 const log = Log.create({ service: "skill" })
 const CLAUDE_EXTERNAL_DIR = ".claude"
@@ -41,23 +41,33 @@ export const Info = Schema.Struct({
 })
 export type Info = Schema.Schema.Type<typeof Info>
 
-export const InvalidError = NamedError.create(
-  "SkillInvalidError",
-  z.object({
-    path: z.string(),
-    message: z.string().optional(),
-    issues: z.custom<z.core.$ZodIssue[]>().optional(),
+const Issue = Schema.StructWithRest(
+  Schema.Struct({
+    message: Schema.String,
+    path: Schema.Array(Schema.String),
   }),
+  [Schema.Record(Schema.String, Schema.Unknown)],
 )
 
-export const NameMismatchError = NamedError.create(
-  "SkillNameMismatchError",
-  z.object({
-    path: z.string(),
-    expected: z.string(),
-    actual: z.string(),
-  }),
-)
+function isSkillFrontmatter(data: unknown): data is { name: string; description?: string } {
+  return (
+    isRecord(data) &&
+    typeof data.name === "string" &&
+    (data.description === undefined || typeof data.description === "string")
+  )
+}
+
+export const InvalidError = NamedError.create("SkillInvalidError", {
+  path: Schema.String,
+  message: Schema.optional(Schema.String),
+  issues: Schema.optional(Schema.Array(Issue)),
+})
+
+export const NameMismatchError = NamedError.create("SkillNameMismatchError", {
+  path: Schema.String,
+  expected: Schema.String,
+  actual: Schema.String,
+})
 
 type State = {
   skills: Record<string, Info>
@@ -101,21 +111,20 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I
 
   if (!md) return
 
-  const parsed = z.object({ name: z.string(), description: z.string().optional() }).safeParse(md.data)
-  if (!parsed.success) return
+  if (!isSkillFrontmatter(md.data)) return
 
-  if (state.skills[parsed.data.name]) {
+  if (state.skills[md.data.name]) {
     log.warn("duplicate skill name", {
-      name: parsed.data.name,
-      existing: state.skills[parsed.data.name].location,
+      name: md.data.name,
+      existing: state.skills[md.data.name].location,
       duplicate: match,
     })
   }
 
   state.dirs.add(path.dirname(match))
-  state.skills[parsed.data.name] = {
-    name: parsed.data.name,
-    description: parsed.data.description,
+  state.skills[md.data.name] = {
+    name: md.data.name,
+    description: md.data.description,
     location: match,
     content: md.content,
   }

+ 4 - 7
packages/opencode/src/storage/db.ts

@@ -7,7 +7,6 @@ import { lazy } from "../util/lazy"
 import { Global } from "@opencode-ai/core/global"
 import * as Log from "@opencode-ai/core/util/log"
 import { NamedError } from "@opencode-ai/core/util/error"
-import z from "zod"
 import path from "path"
 import { readFileSync, readdirSync, existsSync } from "fs"
 import { Flag } from "@opencode-ai/core/flag/flag"
@@ -15,15 +14,13 @@ import { InstallationChannel } from "@opencode-ai/core/installation/version"
 import { InstanceState } from "@/effect/instance-state"
 import { iife } from "@/util/iife"
 import { init } from "#db"
+import { Schema } from "effect"
 
 declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; name: string }[] | undefined
 
-export const NotFoundError = NamedError.create(
-  "NotFoundError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const NotFoundError = NamedError.create("NotFoundError", {
+  message: Schema.String,
+})
 
 const log = Log.create({ service: "db" })
 

+ 3 - 7
packages/opencode/src/storage/storage.ts

@@ -2,7 +2,6 @@ import * as Log from "@opencode-ai/core/util/log"
 import path from "path"
 import { Global } from "@opencode-ai/core/global"
 import { NamedError } from "@opencode-ai/core/util/error"
-import z from "zod"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect"
 import { NonNegativeInt } from "@opencode-ai/core/schema"
@@ -16,12 +15,9 @@ type Migration = (
   git: Git.Interface,
 ) => Effect.Effect<void, AppFileSystem.Error>
 
-export const NotFoundError = NamedError.create(
-  "NotFoundError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const NotFoundError = NamedError.create("NotFoundError", {
+  message: Schema.String,
+})
 
 export type Error = AppFileSystem.Error | InstanceType<typeof NotFoundError>
 

+ 2 - 44
packages/opencode/src/util/named-schema-error.ts

@@ -1,51 +1,9 @@
 import { Schema } from "effect"
+import { NamedError } from "@opencode-ai/core/util/error"
 
 /**
  * Create a Schema-backed NamedError-shaped class.
- *
- * Drop-in replacement for `NamedError.create(tag, zodShape)` but backed by
- * `Schema.Struct` under the hood. The wire shape emitted by the derived
- * `.Schema` is still `{ name: tag, data: {...fields} }` so the generated
- * OpenAPI/SDK output is byte-identical to the original NamedError schema.
- *
- * Preserves the existing surface:
- *   - static `Schema` (Effect schema of the wire shape)
- *   - static `isInstance(x)`
- *   - instance `toObject()` returning `{ name, data }`
- *   - `new X({ ...data }, { cause })`
  */
 export function namedSchemaError<Tag extends string, Fields extends Schema.Struct.Fields>(tag: Tag, fields: Fields) {
-  const dataSchema = Schema.Struct(fields)
-  // Wire shape matches the original NamedError output so the SDK stays stable.
-  const effectSchema = Schema.Struct({
-    name: Schema.Literal(tag),
-    data: dataSchema,
-  }).annotate({ identifier: tag })
-
-  type Data = Schema.Schema.Type<typeof dataSchema>
-
-  class NamedSchemaError extends Error {
-    static readonly Schema = effectSchema
-    static readonly EffectSchema = effectSchema
-    static readonly tag = tag
-    public static isInstance(input: unknown): input is NamedSchemaError {
-      return typeof input === "object" && input !== null && "name" in input && (input as { name: unknown }).name === tag
-    }
-
-    public override readonly name: Tag = tag
-    public readonly data: Data
-
-    constructor(data: Data, options?: ErrorOptions) {
-      super(tag, options)
-      this.data = data
-    }
-
-    toObject(): { name: Tag; data: Data } {
-      return { name: tag, data: this.data }
-    }
-  }
-
-  Object.defineProperty(NamedSchemaError, "name", { value: tag })
-
-  return NamedSchemaError
+  return NamedError.create(tag, fields)
 }

+ 21 - 43
packages/opencode/src/worktree/index.ts

@@ -1,4 +1,3 @@
-import z from "zod"
 import { NamedError } from "@opencode-ai/core/util/error"
 import { Global } from "@opencode-ai/core/global"
 import { InstanceLayer } from "@/project/instance-layer"
@@ -65,54 +64,33 @@ export const ResetInput = Schema.Struct({
 }).annotate({ identifier: "WorktreeResetInput" })
 export type ResetInput = Schema.Schema.Type<typeof ResetInput>
 
-export const NotGitError = NamedError.create(
-  "WorktreeNotGitError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const NotGitError = NamedError.create("WorktreeNotGitError", {
+  message: Schema.String,
+})
 
-export const NameGenerationFailedError = NamedError.create(
-  "WorktreeNameGenerationFailedError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const NameGenerationFailedError = NamedError.create("WorktreeNameGenerationFailedError", {
+  message: Schema.String,
+})
 
-export const CreateFailedError = NamedError.create(
-  "WorktreeCreateFailedError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const CreateFailedError = NamedError.create("WorktreeCreateFailedError", {
+  message: Schema.String,
+})
 
-export const StartCommandFailedError = NamedError.create(
-  "WorktreeStartCommandFailedError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const StartCommandFailedError = NamedError.create("WorktreeStartCommandFailedError", {
+  message: Schema.String,
+})
 
-export const RemoveFailedError = NamedError.create(
-  "WorktreeRemoveFailedError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const RemoveFailedError = NamedError.create("WorktreeRemoveFailedError", {
+  message: Schema.String,
+})
 
-export const ResetFailedError = NamedError.create(
-  "WorktreeResetFailedError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const ResetFailedError = NamedError.create("WorktreeResetFailedError", {
+  message: Schema.String,
+})
 
-export const ListFailedError = NamedError.create(
-  "WorktreeListFailedError",
-  z.object({
-    message: z.string(),
-  }),
-)
+export const ListFailedError = NamedError.create("WorktreeListFailedError", {
+  message: Schema.String,
+})
 
 function slugify(input: string) {
   return input

+ 18 - 0
packages/opencode/test/util/error.test.ts

@@ -1,5 +1,9 @@
 import { describe, expect, test } from "bun:test"
+import { Schema } from "effect"
+import { NamedError } from "@opencode-ai/core/util/error"
 import { errorData, errorFormat, errorMessage } from "../../src/util/error"
+import { namedSchemaError } from "../../src/util/named-schema-error"
+import { UI } from "../../src/cli/ui"
 
 describe("util.error", () => {
   test("formats native Error instances", () => {
@@ -48,4 +52,18 @@ describe("util.error", () => {
     expect(data.message).toBe("ResolveMessage: Cannot resolve module")
     expect(String(data.formatted)).toContain("ResolveMessage")
   })
+
+  test("named schema errors are real NamedError instances", () => {
+    const ExampleError = namedSchemaError("ExampleError", { message: Schema.String })
+    const error = new ExampleError({ message: "boom" })
+
+    expect(error).toBeInstanceOf(NamedError)
+    expect(error.toObject()).toEqual({ name: "ExampleError", data: { message: "boom" } })
+  })
+
+  test("void named errors accept JSON without data", () => {
+    const serialized = JSON.parse(JSON.stringify(new UI.CancelledError(undefined).toObject()))
+
+    expect(Schema.decodeUnknownOption(UI.CancelledError.Schema)(serialized)._tag).toBe("Some")
+  })
 })