Prechádzať zdrojové kódy

research: delete Hono backend (do not merge) (#25667)

Kit Langton 3 mesiacov pred
rodič
commit
28b03595bf
81 zmenil súbory, kde vykonal 225 pridanie a 7567 odobranie
  1. 0 10
      bun.lock
  2. 0 13
      packages/core/src/flag/flag.ts
  3. 2 15
      packages/opencode/package.json
  4. 5 0
      packages/opencode/specs/effect/http-api.md
  5. 0 19
      packages/opencode/src/bus/bus-event.ts
  6. 4 19
      packages/opencode/src/cli/cmd/generate.ts
  7. 3 7
      packages/opencode/src/control-plane/types.ts
  8. 3 7
      packages/opencode/src/control-plane/workspace.ts
  9. 0 44
      packages/opencode/src/server/adapter.bun.ts
  10. 0 75
      packages/opencode/src/server/adapter.node.ts
  11. 0 26
      packages/opencode/src/server/adapter.ts
  12. 0 32
      packages/opencode/src/server/backend.ts
  13. 0 39
      packages/opencode/src/server/error.ts
  14. 0 20
      packages/opencode/src/server/fence.ts
  15. 2 1
      packages/opencode/src/server/httpapi-server.node.ts
  16. 0 91
      packages/opencode/src/server/middleware.ts
  17. 0 149
      packages/opencode/src/server/proxy.ts
  18. 0 160
      packages/opencode/src/server/routes/control/index.ts
  19. 0 228
      packages/opencode/src/server/routes/control/workspace.ts
  20. 0 286
      packages/opencode/src/server/routes/global.ts
  21. 0 8
      packages/opencode/src/server/routes/instance/AGENTS.md
  22. 0 109
      packages/opencode/src/server/routes/instance/config.ts
  23. 0 90
      packages/opencode/src/server/routes/instance/event.ts
  24. 0 419
      packages/opencode/src/server/routes/instance/experimental.ts
  25. 0 190
      packages/opencode/src/server/routes/instance/file.ts
  26. 1 4
      packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts
  27. 1 3
      packages/opencode/src/server/routes/instance/httpapi/server.ts
  28. 0 502
      packages/opencode/src/server/routes/instance/index.ts
  29. 0 277
      packages/opencode/src/server/routes/instance/mcp.ts
  30. 0 32
      packages/opencode/src/server/routes/instance/middleware.ts
  31. 0 73
      packages/opencode/src/server/routes/instance/permission.ts
  32. 0 116
      packages/opencode/src/server/routes/instance/project.ts
  33. 0 158
      packages/opencode/src/server/routes/instance/provider.ts
  34. 0 340
      packages/opencode/src/server/routes/instance/pty.ts
  35. 0 111
      packages/opencode/src/server/routes/instance/question.ts
  36. 0 1124
      packages/opencode/src/server/routes/instance/session.ts
  37. 0 199
      packages/opencode/src/server/routes/instance/sync.ts
  38. 0 59
      packages/opencode/src/server/routes/instance/trace.ts
  39. 0 387
      packages/opencode/src/server/routes/instance/tui.ts
  40. 0 40
      packages/opencode/src/server/routes/ui.ts
  41. 23 204
      packages/opencode/src/server/server.ts
  42. 5 5
      packages/opencode/src/server/shared/tui-control.ts
  43. 0 93
      packages/opencode/src/server/workspace.ts
  44. 1 13
      packages/opencode/src/session/message-v2.ts
  45. 0 22
      packages/opencode/src/sync/index.ts
  46. 4 14
      packages/opencode/src/worktree/index.ts
  47. 5 4
      packages/opencode/test/control-plane/workspace.test.ts
  48. 0 86
      packages/opencode/test/project/instance-bootstrap-regression.test.ts
  49. 0 501
      packages/opencode/test/server/httpapi-bridge.test.ts
  50. 0 5
      packages/opencode/test/server/httpapi-compression.test.ts
  51. 0 4
      packages/opencode/test/server/httpapi-config.test.ts
  52. 7 23
      packages/opencode/test/server/httpapi-cors-vary.test.ts
  53. 15 9
      packages/opencode/test/server/httpapi-cors.test.ts
  54. 7 16
      packages/opencode/test/server/httpapi-event.test.ts
  55. 0 10
      packages/opencode/test/server/httpapi-exercise/backend.ts
  56. 0 1
      packages/opencode/test/server/httpapi-exercise/environment.ts
  57. 7 9
      packages/opencode/test/server/httpapi-exercise/index.ts
  58. 1 2
      packages/opencode/test/server/httpapi-exercise/report.ts
  59. 1 1
      packages/opencode/test/server/httpapi-exercise/routing.ts
  60. 0 54
      packages/opencode/test/server/httpapi-exercise/runner.ts
  61. 0 3
      packages/opencode/test/server/httpapi-exercise/runtime.ts
  62. 2 2
      packages/opencode/test/server/httpapi-exercise/types.ts
  63. 0 4
      packages/opencode/test/server/httpapi-experimental.test.ts
  64. 0 122
      packages/opencode/test/server/httpapi-instance.legacy.test.ts
  65. 5 9
      packages/opencode/test/server/httpapi-instance.test.ts
  66. 0 254
      packages/opencode/test/server/httpapi-json-parity.test.ts
  67. 16 38
      packages/opencode/test/server/httpapi-listen.test.ts
  68. 5 13
      packages/opencode/test/server/httpapi-mcp.test.ts
  69. 0 127
      packages/opencode/test/server/httpapi-parity.test.ts
  70. 14 34
      packages/opencode/test/server/httpapi-provider.test.ts
  71. 2 20
      packages/opencode/test/server/httpapi-pty.test.ts
  72. 0 4
      packages/opencode/test/server/httpapi-raw-route-auth.test.ts
  73. 53 55
      packages/opencode/test/server/httpapi-sdk.test.ts
  74. 23 50
      packages/opencode/test/server/httpapi-session.test.ts
  75. 6 15
      packages/opencode/test/server/httpapi-sync.test.ts
  76. 0 129
      packages/opencode/test/server/httpapi-tui.test.ts
  77. 0 13
      packages/opencode/test/server/httpapi-ui.test.ts
  78. 1 30
      packages/opencode/test/server/httpapi-workspace.test.ts
  79. 0 76
      packages/opencode/test/server/trace-attributes.test.ts
  80. 0 3
      packages/opencode/test/server/worktree-endpoint-repro.test.ts
  81. 1 8
      packages/sdk/js/script/build.ts

+ 0 - 10
bun.lock

@@ -393,10 +393,6 @@
         "@effect/opentelemetry": "catalog:",
         "@effect/platform-node": "catalog:",
         "@gitlab/opencode-gitlab-auth": "1.3.3",
-        "@hono/node-server": "1.19.11",
-        "@hono/node-ws": "1.3.0",
-        "@hono/standard-validator": "0.1.5",
-        "@hono/zod-validator": "catalog:",
         "@lydell/node-pty": "catalog:",
         "@modelcontextprotocol/sdk": "1.27.1",
         "@octokit/graphql": "9.0.2",
@@ -437,8 +433,6 @@
         "glob": "13.0.5",
         "google-auth-library": "10.5.0",
         "gray-matter": "4.0.3",
-        "hono": "catalog:",
-        "hono-openapi": "catalog:",
         "ignore": "7.0.5",
         "immer": "11.1.4",
         "jsonc-parser": "3.3.1",
@@ -465,7 +459,6 @@
         "xdg-basedir": "5.1.0",
         "yargs": "18.0.0",
         "zod": "catalog:",
-        "zod-to-json-schema": "3.24.5",
       },
       "devDependencies": {
         "@babel/core": "7.28.4",
@@ -498,7 +491,6 @@
         "typescript": "catalog:",
         "vscode-languageserver-types": "3.17.5",
         "why-is-node-running": "3.2.2",
-        "zod-to-json-schema": "3.24.5",
       },
     },
     "packages/plugin": {
@@ -1239,8 +1231,6 @@
 
     "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
 
-    "@hono/node-ws": ["@hono/node-ws@1.3.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.19.2", "hono": "^4.6.0" } }, "sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q=="],
-
     "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
 
     "@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],

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

@@ -1,5 +1,4 @@
 import { Config } from "effect"
-import { InstallationChannel } from "../installation/version"
 
 function truthy(key: string) {
   const value = process.env[key]?.toLowerCase()
@@ -11,10 +10,6 @@ function falsy(key: string) {
   return value === "false" || value === "0"
 }
 
-// Channels that default to the new effect-httpapi server backend. The legacy
-// hono backend remains the default for stable (`prod`/`latest`) installs.
-const HTTPAPI_DEFAULT_ON_CHANNELS = new Set(["dev", "beta", "local"])
-
 function number(key: string) {
   const value = process.env[key]
   if (!value) return undefined
@@ -88,14 +83,6 @@ export const Flag = {
   OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"),
 
   OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
-  // Defaults to true on dev/beta/local channels so internal users exercise the
-  // new effect-httpapi server backend. Stable (`prod`/`latest`) installs stay
-  // on the legacy hono backend until the rollout is complete. An explicit env
-  // var ("true"/"1" or "false"/"0") always wins, providing an opt-in for
-  // stable users and an escape hatch for dev/beta users.
-  OPENCODE_EXPERIMENTAL_HTTPAPI:
-    truthy("OPENCODE_EXPERIMENTAL_HTTPAPI") ||
-    (!falsy("OPENCODE_EXPERIMENTAL_HTTPAPI") && HTTPAPI_DEFAULT_ON_CHANNELS.has(InstallationChannel)),
   OPENCODE_EXPERIMENTAL_WORKSPACES: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WORKSPACES"),
   OPENCODE_EXPERIMENTAL_EVENT_SYSTEM: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EVENT_SYSTEM"),
 

+ 2 - 15
packages/opencode/package.json

@@ -33,11 +33,6 @@
       "node": "./src/pty/pty.node.ts",
       "default": "./src/pty/pty.bun.ts"
     },
-    "#hono": {
-      "bun": "./src/server/adapter.bun.ts",
-      "node": "./src/server/adapter.node.ts",
-      "default": "./src/server/adapter.bun.ts"
-    },
     "#httpapi-server": {
       "bun": "./src/server/httpapi-server.node.ts",
       "node": "./src/server/httpapi-server.node.ts",
@@ -74,8 +69,7 @@
     "prettier": "3.6.2",
     "typescript": "catalog:",
     "vscode-languageserver-types": "3.17.5",
-    "why-is-node-running": "3.2.2",
-    "zod-to-json-schema": "3.24.5"
+    "why-is-node-running": "3.2.2"
   },
   "dependencies": {
     "@actions/core": "1.11.1",
@@ -106,10 +100,6 @@
     "@effect/opentelemetry": "catalog:",
     "@effect/platform-node": "catalog:",
     "@gitlab/opencode-gitlab-auth": "1.3.3",
-    "@hono/node-server": "1.19.11",
-    "@hono/node-ws": "1.3.0",
-    "@hono/standard-validator": "0.1.5",
-    "@hono/zod-validator": "catalog:",
     "@lydell/node-pty": "catalog:",
     "@modelcontextprotocol/sdk": "1.27.1",
     "@octokit/graphql": "9.0.2",
@@ -150,8 +140,6 @@
     "glob": "13.0.5",
     "google-auth-library": "10.5.0",
     "gray-matter": "4.0.3",
-    "hono": "catalog:",
-    "hono-openapi": "catalog:",
     "ignore": "7.0.5",
     "immer": "11.1.4",
     "jsonc-parser": "3.3.1",
@@ -177,8 +165,7 @@
     "which": "6.0.1",
     "xdg-basedir": "5.1.0",
     "yargs": "18.0.0",
-    "zod": "catalog:",
-    "zod-to-json-schema": "3.24.5"
+    "zod": "catalog:"
   },
   "overrides": {
     "drizzle-orm": "catalog:"

+ 5 - 0
packages/opencode/specs/effect/http-api.md

@@ -1,5 +1,10 @@
 # HttpApi migration
 
+> Historical migration notes. PR #25667 deletes the Hono backend and removes the
+> `OPENCODE_EXPERIMENTAL_HTTPAPI` backend selection path, so sections below that
+> describe Hono as the default, SDK generation from `hono-openapi`, bridge parity,
+> or dual-backend rollout are pre-deletion context rather than current guidance.
+
 Plan for replacing instance Hono route implementations with Effect `HttpApi` while preserving behavior, OpenAPI, and SDK output during the transition.
 
 ## End State

+ 0 - 19
packages/opencode/src/bus/bus-event.ts

@@ -1,6 +1,4 @@
-import z from "zod"
 import { Schema } from "effect"
-import { zodObject } from "@/util/effect-zod"
 
 export type Definition<Type extends string = string, Properties extends Schema.Top = Schema.Top> = {
   type: Type
@@ -18,23 +16,6 @@ export function define<Type extends string, Properties extends Schema.Top>(
   return result
 }
 
-export function payloads() {
-  return registry
-    .entries()
-    .map(([type, def]) => {
-      return z
-        .object({
-          id: z.string(),
-          type: z.literal(type),
-          properties: zodObject(def.properties),
-        })
-        .meta({
-          ref: `Event.${def.type}`,
-        })
-    })
-    .toArray()
-}
-
 export function effectPayloads() {
   return registry
     .entries()

+ 4 - 19
packages/opencode/src/cli/cmd/generate.ts

@@ -1,28 +1,13 @@
 import { Server } from "../../server/server"
 import type { CommandModule } from "yargs"
 
-type Args = {
-  httpapi: boolean
-  hono: boolean
-}
+type Args = {}
 
 export const GenerateCommand = {
   command: "generate",
-  builder: (yargs) =>
-    yargs
-      .option("httpapi", {
-        type: "boolean",
-        default: false,
-        description:
-          "Generate OpenAPI from the Effect HttpApi contract (default; flag retained for backwards compatibility)",
-      })
-      .option("hono", {
-        type: "boolean",
-        default: false,
-        description: "Generate OpenAPI from the legacy Hono backend (parity-diff only; will be removed)",
-      }),
-  handler: async (args) => {
-    const specs = args.hono ? await Server.openapiHono() : await Server.openapi()
+  builder: (yargs) => yargs,
+  handler: async () => {
+    const specs = (await Server.openapi()) as { paths: Record<string, Record<string, any>> }
     for (const item of Object.values(specs.paths)) {
       for (const method of ["get", "post", "put", "delete", "patch"] as const) {
         const operation = item[method]

+ 3 - 7
packages/opencode/src/control-plane/types.ts

@@ -1,8 +1,7 @@
 import { Schema, Struct } from "effect"
 import { ProjectID } from "@/project/schema"
 import { WorkspaceID } from "./schema"
-import { zod } from "@/util/effect-zod"
-import { type DeepMutable, withStatics } from "@/util/schema"
+import type { DeepMutable } from "@/util/schema"
 
 export const WorkspaceInfo = Schema.Struct({
   id: WorkspaceID,
@@ -12,21 +11,18 @@ export const WorkspaceInfo = Schema.Struct({
   directory: Schema.NullOr(Schema.String),
   extra: Schema.NullOr(Schema.Unknown),
   projectID: ProjectID,
-})
-  .annotate({ identifier: "Workspace" })
-  .pipe(withStatics((s) => ({ zod: zod(s) })))
+}).annotate({ identifier: "Workspace" })
 export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
 
 export const WorkspaceListedInfo = Schema.Struct(Struct.omit(WorkspaceInfo.fields, ["id"]))
   .annotate({ identifier: "WorkspaceListedInfo" })
-  .pipe(withStatics((s) => ({ zod: zod(s) })))
 export type WorkspaceListedInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceListedInfo>>
 
 export const WorkspaceAdapterEntry = Schema.Struct({
   type: Schema.String,
   name: Schema.String,
   description: Schema.String,
-}).pipe(withStatics((s) => ({ zod: zod(s) })))
+})
 export type WorkspaceAdapterEntry = Schema.Schema.Type<typeof WorkspaceAdapterEntry>
 
 export type Target =

+ 3 - 7
packages/opencode/src/control-plane/workspace.ts

@@ -28,8 +28,6 @@ import { errorData } from "@/util/error"
 import { waitEvent } from "./util"
 import { WorkspaceContext } from "./workspace-context"
 import { EffectBridge } from "@/effect/bridge"
-import { withStatics } from "@/util/schema"
-import { zod as effectZod, zodObject } from "@/util/effect-zod"
 import { Vcs } from "@/project/vcs"
 import { InstanceStore } from "@/project/instance-store"
 import { InstanceBootstrap } from "@/project/bootstrap"
@@ -37,9 +35,7 @@ import { InstanceBootstrap } from "@/project/bootstrap"
 export const Info = Schema.Struct({
   ...WorkspaceInfoSchema.fields,
   timeUsed: Schema.Number,
-})
-  .annotate({ identifier: "Workspace" })
-  .pipe(withStatics((s) => ({ zod: effectZod(s) })))
+}).annotate({ identifier: "Workspace" })
 export type Info = WorkspaceInfo & { timeUsed: number }
 
 export const ConnectionStatus = Schema.Struct({
@@ -88,14 +84,14 @@ export const CreateInput = Schema.Struct({
   branch: Info.fields.branch,
   projectID: ProjectID,
   extra: Schema.optional(Info.fields.extra),
-}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
+})
 export type CreateInput = Schema.Schema.Type<typeof CreateInput>
 
 export const SessionWarpInput = Schema.Struct({
   workspaceID: Schema.NullOr(WorkspaceID),
   sessionID: SessionID,
   copyChanges: Schema.optional(Schema.Boolean),
-}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
+})
 export type SessionWarpInput = Schema.Schema.Type<typeof SessionWarpInput>
 
 export class SyncHttpError extends Schema.TaggedErrorClass<SyncHttpError>()("WorkspaceSyncHttpError", {

+ 0 - 44
packages/opencode/src/server/adapter.bun.ts

@@ -1,44 +0,0 @@
-import type { Hono } from "hono"
-import { createBunWebSocket } from "hono/bun"
-import type { Adapter, FetchApp, Opts } from "./adapter"
-
-function listen(app: FetchApp, opts: Opts, websocket?: ReturnType<typeof createBunWebSocket>["websocket"]) {
-  const start = (port: number) => {
-    try {
-      if (websocket) {
-        return Bun.serve({ fetch: app.fetch, hostname: opts.hostname, idleTimeout: 0, websocket, port })
-      }
-      return Bun.serve({ fetch: app.fetch, hostname: opts.hostname, idleTimeout: 0, port })
-    } catch {
-      return
-    }
-  }
-  const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
-  if (!server) {
-    throw new Error(`Failed to start server on port ${opts.port}`)
-  }
-  if (!server.port) {
-    throw new Error(`Failed to resolve server address for port ${opts.port}`)
-  }
-  return {
-    port: server.port,
-    stop(close?: boolean) {
-      return Promise.resolve(server.stop(close))
-    },
-  }
-}
-
-export const adapter: Adapter = {
-  create(app: Hono) {
-    const ws = createBunWebSocket()
-    return {
-      upgradeWebSocket: ws.upgradeWebSocket,
-      listen: (opts) => Promise.resolve(listen(app, opts, ws.websocket)),
-    }
-  },
-  createFetch(app) {
-    return {
-      listen: (opts) => Promise.resolve(listen(app, opts)),
-    }
-  },
-}

+ 0 - 75
packages/opencode/src/server/adapter.node.ts

@@ -1,75 +0,0 @@
-import { EventEmitter } from "node:events"
-import { createAdaptorServer, type ServerType } from "@hono/node-server"
-import { createNodeWebSocket } from "@hono/node-ws"
-import type { Hono } from "hono"
-import type { Adapter, FetchApp, Opts } from "./adapter"
-
-async function listen(app: FetchApp, opts: Opts, inject?: (server: ServerType) => void) {
-  const start = (port: number) =>
-    new Promise<ServerType>((resolve, reject) => {
-      const server = createAdaptorServer({ fetch: app.fetch })
-      const events = server as EventEmitter
-      inject?.(server)
-      const fail = (err: Error) => {
-        cleanup()
-        reject(err)
-      }
-      const ready = () => {
-        cleanup()
-        resolve(server)
-      }
-      const cleanup = () => {
-        events.off("error", fail)
-        events.off("listening", ready)
-      }
-      events.once("error", fail)
-      events.once("listening", ready)
-      server.listen(port, opts.hostname)
-    })
-
-  const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
-  const addr = server.address()
-  if (!addr || typeof addr === "string") {
-    throw new Error(`Failed to resolve server address for port ${opts.port}`)
-  }
-
-  let closing: Promise<void> | undefined
-  return {
-    port: addr.port,
-    stop(close?: boolean) {
-      closing ??= new Promise<void>((resolve, reject) => {
-        server.close((err) => {
-          if (err) {
-            reject(err)
-            return
-          }
-          resolve()
-        })
-        if (close) {
-          if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
-            server.closeAllConnections()
-          }
-          if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
-            server.closeIdleConnections()
-          }
-        }
-      })
-      return closing
-    },
-  }
-}
-
-export const adapter: Adapter = {
-  create(app: Hono) {
-    const ws = createNodeWebSocket({ app })
-    return {
-      upgradeWebSocket: ws.upgradeWebSocket,
-      listen: (opts) => listen(app, opts, ws.injectWebSocket),
-    }
-  },
-  createFetch(app) {
-    return {
-      listen: (opts) => listen(app, opts),
-    }
-  },
-}

+ 0 - 26
packages/opencode/src/server/adapter.ts

@@ -1,26 +0,0 @@
-import type { Hono } from "hono"
-import type { UpgradeWebSocket } from "hono/ws"
-
-export type FetchApp = {
-  fetch(request: Request): Response | Promise<Response>
-}
-
-export type Opts = {
-  port: number
-  hostname: string
-}
-
-export type Listener = {
-  port: number
-  stop: (close?: boolean) => Promise<void>
-}
-
-export interface Runtime {
-  upgradeWebSocket: UpgradeWebSocket
-  listen(opts: Opts): Promise<Listener>
-}
-
-export interface Adapter {
-  create(app: Hono): Runtime
-  createFetch(app: FetchApp): Omit<Runtime, "upgradeWebSocket">
-}

+ 0 - 32
packages/opencode/src/server/backend.ts

@@ -1,32 +0,0 @@
-import { Flag } from "@opencode-ai/core/flag/flag"
-import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
-
-export type Backend = "effect-httpapi" | "hono"
-
-export type Selection = {
-  backend: Backend
-  reason: "env" | "stable" | "explicit"
-}
-
-export type Attributes = ReturnType<typeof attributes>
-
-export function select(): Selection {
-  if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) return { backend: "effect-httpapi", reason: "env" }
-  return { backend: "hono", reason: "stable" }
-}
-
-export function attributes(selection: Selection): Record<string, string> {
-  return {
-    "opencode.server.backend": selection.backend,
-    "opencode.server.backend.reason": selection.reason,
-    "opencode.installation.channel": InstallationChannel,
-    "opencode.installation.version": InstallationVersion,
-  }
-}
-
-export function force(selection: Selection, backend: Backend): Selection {
-  return {
-    backend,
-    reason: selection.backend === backend ? selection.reason : "explicit",
-  }
-}

+ 0 - 39
packages/opencode/src/server/error.ts

@@ -1,39 +0,0 @@
-import { resolver } from "hono-openapi"
-import z from "zod"
-import { NotFoundError } from "@/storage/storage"
-
-export const ERRORS = {
-  400: {
-    description: "Bad request",
-    content: {
-      "application/json": {
-        schema: resolver(
-          z
-            .object({
-              data: z.any(),
-              errors: z.array(z.record(z.string(), z.any())),
-              success: z.literal(false),
-            })
-            .meta({
-              ref: "BadRequestError",
-            }),
-        ),
-      },
-    },
-  },
-  403: {
-    description: "Forbidden",
-  },
-  404: {
-    description: "Not found",
-    content: {
-      "application/json": {
-        schema: resolver(NotFoundError.Schema),
-      },
-    },
-  },
-} as const
-
-export function errors(...codes: number[]) {
-  return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]]))
-}

+ 0 - 20
packages/opencode/src/server/fence.ts

@@ -1,20 +0,0 @@
-import type { MiddlewareHandler } from "hono"
-import * as Log from "@opencode-ai/core/util/log"
-import { HEADER, diff, load } from "./shared/fence"
-
-const log = Log.create({ service: "fence-middleware" })
-
-export const FenceMiddleware: MiddlewareHandler = async (c, next) => {
-  if (c.req.method === "GET" || c.req.method === "HEAD" || c.req.method === "OPTIONS") return next()
-
-  const prev = load()
-  await next()
-  const current = diff(prev, load())
-
-  if (Object.keys(current).length > 0) {
-    log.info("header", {
-      diff: current,
-    })
-    c.res.headers.set(HEADER, JSON.stringify(current))
-  }
-}

+ 2 - 1
packages/opencode/src/server/httpapi-server.node.ts

@@ -1,13 +1,14 @@
 import { NodeHttpServer } from "@effect/platform-node"
 import { Effect, Layer } from "effect"
 import { createServer } from "node:http"
-import type { Opts } from "./adapter"
 import { Service } from "./httpapi-server"
 
 export { Service }
 
 export const name = "node-http-server"
 
+export type Opts = { port: number; hostname: string }
+
 export const layer = (opts: Opts) => {
   const server = createServer()
   const serverRef = { closeStarted: false, forceStop: false }

+ 0 - 91
packages/opencode/src/server/middleware.ts

@@ -1,91 +0,0 @@
-import { Provider } from "@/provider/provider"
-import { NamedError } from "@opencode-ai/core/util/error"
-import { NotFoundError } from "@/storage/storage"
-import { Session } from "@/session/session"
-import type { ContentfulStatusCode } from "hono/utils/http-status"
-import type { ErrorHandler, MiddlewareHandler } from "hono"
-import { HTTPException } from "hono/http-exception"
-import * as Log from "@opencode-ai/core/util/log"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import { basicAuth } from "hono/basic-auth"
-import { cors } from "hono/cors"
-import { compress } from "hono/compress"
-import * as ServerBackend from "./backend"
-import { isAllowedCorsOrigin, type CorsOptions } from "./cors"
-import { isPtyConnectPath, PTY_CONNECT_TICKET_QUERY } from "./shared/pty-ticket"
-import { isPublicUIPath } from "./shared/public-ui"
-
-const log = Log.create({ service: "server" })
-
-export const ErrorMiddleware: ErrorHandler = (err, c) => {
-  log.error("failed", {
-    error: err,
-  })
-  if (err instanceof NamedError) {
-    let status: ContentfulStatusCode
-    if (err instanceof NotFoundError) status = 404
-    else if (err instanceof Provider.ModelNotFoundError) status = 400
-    else if (err.name === "ProviderAuthValidationFailed") status = 400
-    else if (err.name.startsWith("Worktree")) status = 400
-    else status = 500
-    return c.json(err.toObject(), { status })
-  }
-  if (err instanceof Session.BusyError) {
-    return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 })
-  }
-  if (err instanceof HTTPException) return err.getResponse()
-  const message = err instanceof Error && err.stack ? err.stack : err.toString()
-  return c.json(new NamedError.Unknown({ message }).toObject(), {
-    status: 500,
-  })
-}
-
-export const AuthMiddleware: MiddlewareHandler = (c, next) => {
-  // Allow CORS preflight requests to succeed without auth.
-  // Browser clients sending Authorization headers will preflight with OPTIONS.
-  if (c.req.method === "OPTIONS") return next()
-  const password = Flag.OPENCODE_SERVER_PASSWORD
-  if (!password) return next()
-  if (isPublicUIPath(c.req.method, c.req.path)) return next()
-  if (isPtyConnectPath(c.req.path) && c.req.query(PTY_CONNECT_TICKET_QUERY)) return next()
-  const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
-
-  if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
-
-  return basicAuth({ username, password })(c, next)
-}
-
-export function LoggerMiddleware(backendAttributes: ServerBackend.Attributes): MiddlewareHandler {
-  return async (c, next) => {
-    const skip = c.req.path === "/log"
-    if (skip) return next()
-    const attributes = {
-      method: c.req.method,
-      path: c.req.path,
-      // If this logger grows full-URL fields, redact auth_token and ticket query params.
-      ...backendAttributes,
-    }
-    log.info("request", attributes)
-    const timer = log.time("request", attributes)
-    await next()
-    timer.stop()
-  }
-}
-
-export function CorsMiddleware(opts?: CorsOptions): MiddlewareHandler {
-  return cors({
-    maxAge: 86_400,
-    origin(input) {
-      if (isAllowedCorsOrigin(input, opts)) return input
-    },
-  })
-}
-
-const zipped = compress()
-export const CompressionMiddleware: MiddlewareHandler = (c, next) => {
-  const path = c.req.path
-  const method = c.req.method
-  if (path === "/event" || path === "/global/event") return next()
-  if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next()
-  return zipped(c, next)
-}

+ 0 - 149
packages/opencode/src/server/proxy.ts

@@ -1,149 +0,0 @@
-import { Hono } from "hono"
-import type { UpgradeWebSocket } from "hono/ws"
-import * as Log from "@opencode-ai/core/util/log"
-import * as Fence from "./shared/fence"
-import type { WorkspaceID } from "@/control-plane/schema"
-import { Workspace } from "@/control-plane/workspace"
-import { AppRuntime } from "@/effect/app-runtime"
-import { ProxyUtil } from "./proxy-util"
-import { Effect, Stream } from "effect"
-import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http"
-
-type Msg = string | ArrayBuffer | Uint8Array
-
-function send(ws: { send(data: string | ArrayBuffer | Uint8Array): void }, data: any) {
-  if (data instanceof Blob) {
-    return data.arrayBuffer().then((x) => ws.send(x))
-  }
-  return ws.send(data)
-}
-
-const app = (upgrade: UpgradeWebSocket) =>
-  new Hono().get(
-    "/__workspace_ws",
-    upgrade((c) => {
-      const url = c.req.header("x-opencode-proxy-url")
-      const queue: Msg[] = []
-      let remote: WebSocket | undefined
-      return {
-        onOpen(_, ws) {
-          if (!url) {
-            ws.close(1011, "missing proxy target")
-            return
-          }
-          remote = new WebSocket(url, ProxyUtil.websocketProtocols(c.req.raw))
-          remote.binaryType = "arraybuffer"
-          remote.onopen = () => {
-            for (const item of queue) remote?.send(item)
-            queue.length = 0
-          }
-          remote.onmessage = (event) => {
-            void send(ws, event.data)
-          }
-          remote.onerror = () => {
-            ws.close(1011, "proxy error")
-          }
-          remote.onclose = (event) => {
-            ws.close(event.code, event.reason)
-          }
-        },
-        onMessage(event) {
-          const data = event.data
-          if (typeof data !== "string" && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer)) return
-          if (remote?.readyState === WebSocket.OPEN) {
-            remote.send(data)
-            return
-          }
-          queue.push(data)
-        },
-        onClose(event) {
-          remote?.close(event.code, event.reason)
-        },
-      }
-    }),
-  )
-
-const log = Log.create({ service: "server-proxy" })
-
-function statusText(response: unknown) {
-  return (response as { source?: Response }).source?.statusText
-}
-
-export function httpEffect(url: string | URL, extra: HeadersInit | undefined, req: Request, workspaceID: WorkspaceID) {
-  return Effect.gen(function* () {
-    const syncing = yield* Workspace.Service.use((workspace) => workspace.isSyncing(workspaceID))
-    if (!syncing) {
-      return new Response(`broken sync connection for workspace: ${workspaceID}`, {
-        status: 503,
-        headers: {
-          "content-type": "text/plain; charset=utf-8",
-        },
-      })
-    }
-
-    const response = yield* HttpClient.execute(
-      HttpClientRequest.make(req.method as never)(url, {
-        headers: ProxyUtil.headers(req, extra),
-        body:
-          req.method === "GET" || req.method === "HEAD"
-            ? HttpBody.empty
-            : HttpBody.raw(req.body, {
-                contentType: req.headers.get("content-type") ?? undefined,
-                contentLength: req.headers.get("content-length")
-                  ? Number(req.headers.get("content-length"))
-                  : undefined,
-              }),
-      }),
-    )
-    const next = new Headers(response.headers as HeadersInit)
-    const sync = Fence.parse(next)
-    next.delete("content-encoding")
-    next.delete("content-length")
-
-    if (sync) yield* Fence.waitEffect(workspaceID, sync, req.signal)
-    const body = yield* Stream.toReadableStreamEffect(response.stream.pipe(Stream.catchCause(() => Stream.empty)))
-    return new Response(body, {
-      status: response.status,
-      statusText: statusText(response),
-      headers: next,
-    })
-  }).pipe(
-    Effect.provide(FetchHttpClient.layer),
-    Effect.catch(() => Effect.succeed(new Response(null, { status: 500 }))),
-  )
-}
-
-export function http(url: string | URL, extra: HeadersInit | undefined, req: Request, workspaceID: WorkspaceID) {
-  return AppRuntime.runPromise(httpEffect(url, extra, req, workspaceID))
-}
-
-export function websocket(
-  upgrade: UpgradeWebSocket,
-  target: string | URL,
-  extra: HeadersInit | undefined,
-  req: Request,
-  env: unknown,
-) {
-  const proxy = new URL(req.url)
-  proxy.pathname = "/__workspace_ws"
-  proxy.search = ""
-  const next = new Headers(req.headers)
-  next.set("x-opencode-proxy-url", ProxyUtil.websocketTargetURL(target))
-  for (const [key, value] of new Headers(extra).entries()) {
-    next.set(key, value)
-  }
-  log.info("proxy websocket", {
-    request: req.url,
-    target: String(target),
-  })
-  return app(upgrade).fetch(
-    new Request(proxy, {
-      method: req.method,
-      headers: next,
-      signal: req.signal,
-    }),
-    env as never,
-  )
-}
-
-export * as ServerProxy from "./proxy"

+ 0 - 160
packages/opencode/src/server/routes/control/index.ts

@@ -1,160 +0,0 @@
-import { Auth } from "@/auth"
-import { AppRuntime } from "@/effect/app-runtime"
-import * as Log from "@opencode-ai/core/util/log"
-import { Effect } from "effect"
-import { ProviderID } from "@/provider/schema"
-import { Hono } from "hono"
-import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi"
-import z from "zod"
-import { errors } from "../../error"
-
-export function ControlPlaneRoutes(): Hono {
-  const app = new Hono()
-  return app
-    .put(
-      "/auth/:providerID",
-      describeRoute({
-        summary: "Set auth credentials",
-        description: "Set authentication credentials",
-        operationId: "auth.set",
-        responses: {
-          200: {
-            description: "Successfully set authentication credentials",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          providerID: ProviderID.zod,
-        }),
-      ),
-      validator("json", Auth.Info.zod),
-      async (c) => {
-        const providerID = c.req.valid("param").providerID
-        const info = c.req.valid("json")
-        await AppRuntime.runPromise(
-          Effect.gen(function* () {
-            const auth = yield* Auth.Service
-            yield* auth.set(providerID, info)
-          }),
-        )
-        return c.json(true)
-      },
-    )
-    .delete(
-      "/auth/:providerID",
-      describeRoute({
-        summary: "Remove auth credentials",
-        description: "Remove authentication credentials",
-        operationId: "auth.remove",
-        responses: {
-          200: {
-            description: "Successfully removed authentication credentials",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          providerID: ProviderID.zod,
-        }),
-      ),
-      async (c) => {
-        const providerID = c.req.valid("param").providerID
-        await AppRuntime.runPromise(
-          Effect.gen(function* () {
-            const auth = yield* Auth.Service
-            yield* auth.remove(providerID)
-          }),
-        )
-        return c.json(true)
-      },
-    )
-    .get(
-      "/doc",
-      openAPIRouteHandler(app, {
-        documentation: {
-          info: {
-            title: "opencode",
-            version: "0.0.3",
-            description: "opencode api",
-          },
-          openapi: "3.1.1",
-        },
-      }),
-    )
-    .use(
-      validator(
-        "query",
-        z.object({
-          directory: z.string().optional(),
-          workspace: z.string().optional(),
-        }),
-      ),
-    )
-    .post(
-      "/log",
-      describeRoute({
-        summary: "Write log",
-        description: "Write a log entry to the server logs with specified level and metadata.",
-        operationId: "app.log",
-        responses: {
-          200: {
-            description: "Log entry written successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "json",
-        z.object({
-          service: z.string().meta({ description: "Service name for the log entry" }),
-          level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
-          message: z.string().meta({ description: "Log message" }),
-          extra: z
-            .record(z.string(), z.any())
-            .optional()
-            .meta({ description: "Additional metadata for the log entry" }),
-        }),
-      ),
-      async (c) => {
-        const { service, level, message, extra } = c.req.valid("json")
-        const logger = Log.create({ service })
-
-        switch (level) {
-          case "debug":
-            logger.debug(message, extra)
-            break
-          case "info":
-            logger.info(message, extra)
-            break
-          case "error":
-            logger.error(message, extra)
-            break
-          case "warn":
-            logger.warn(message, extra)
-            break
-        }
-
-        return c.json(true)
-      },
-    )
-}

+ 0 - 228
packages/opencode/src/server/routes/control/workspace.ts

@@ -1,228 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, resolver, validator } from "hono-openapi"
-import z from "zod"
-import { Effect } from "effect"
-import { listAdapters } from "@/control-plane/adapters"
-import { Workspace } from "@/control-plane/workspace"
-import { AppRuntime } from "@/effect/app-runtime"
-import { WorkspaceAdapterEntry } from "@/control-plane/types"
-import { zodObject } from "@/util/effect-zod"
-import { Instance } from "@/project/instance"
-import { Vcs } from "@/project/vcs"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-
-export const WorkspaceRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/adapter",
-      describeRoute({
-        summary: "List workspace adapters",
-        description: "List all available workspace adapters for the current project.",
-        operationId: "experimental.workspace.adapter.list",
-        responses: {
-          200: {
-            description: "Workspace adapters",
-            content: {
-              "application/json": {
-                schema: resolver(z.array(zodObject(WorkspaceAdapterEntry))),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        return c.json(await listAdapters(Instance.project.id))
-      },
-    )
-    .post(
-      "/",
-      describeRoute({
-        summary: "Create workspace",
-        description: "Create a workspace for the current project.",
-        operationId: "experimental.workspace.create",
-        responses: {
-          200: {
-            description: "Workspace created",
-            content: {
-              "application/json": {
-                schema: resolver(Workspace.Info.zod),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "json",
-        Workspace.CreateInput.zodObject.omit({
-          projectID: true,
-        }),
-      ),
-      async (c) => {
-        const body = c.req.valid("json") as Omit<Workspace.CreateInput, "projectID">
-        const workspace = await AppRuntime.runPromise(
-          Workspace.Service.use((svc) =>
-            svc.create({
-              projectID: Instance.project.id,
-              ...body,
-            }),
-          ),
-        )
-        return c.json(workspace)
-      },
-    )
-    .get(
-      "/",
-      describeRoute({
-        summary: "List workspaces",
-        description: "List all workspaces.",
-        operationId: "experimental.workspace.list",
-        responses: {
-          200: {
-            description: "Workspaces",
-            content: {
-              "application/json": {
-                schema: resolver(z.array(Workspace.Info.zod)),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        return c.json(await AppRuntime.runPromise(Workspace.Service.use((svc) => svc.list(Instance.project))))
-      },
-    )
-    .post(
-      "/sync-list",
-      describeRoute({
-        summary: "Sync workspace list",
-        description: "Register missing workspaces returned by workspace adapters.",
-        operationId: "experimental.workspace.syncList",
-        responses: {
-          204: {
-            description: "Workspace list synced",
-          },
-        },
-      }),
-      async (c) => {
-        await AppRuntime.runPromise(Workspace.Service.use((svc) => svc.syncList(Instance.project)))
-        return c.body(null, 204)
-      },
-    )
-    .get(
-      "/status",
-      describeRoute({
-        summary: "Workspace status",
-        description: "Get connection status for workspaces in the current project.",
-        operationId: "experimental.workspace.status",
-        responses: {
-          200: {
-            description: "Workspace status",
-            content: {
-              "application/json": {
-                schema: resolver(z.array(zodObject(Workspace.ConnectionStatus))),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        const result = await AppRuntime.runPromise(
-          Workspace.Service.use((svc) => Effect.all([svc.list(Instance.project), svc.status()])),
-        )
-        const ids = new Set(result[0].map((item) => item.id))
-        return c.json(result[1].filter((item) => ids.has(item.workspaceID)))
-      },
-    )
-    .delete(
-      "/:id",
-      describeRoute({
-        summary: "Remove workspace",
-        description: "Remove an existing workspace.",
-        operationId: "experimental.workspace.remove",
-        responses: {
-          200: {
-            description: "Workspace removed",
-            content: {
-              "application/json": {
-                schema: resolver(Workspace.Info.zod.optional()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          id: zodObject(Workspace.Info).shape.id,
-        }),
-      ),
-      async (c) => {
-        const { id } = c.req.valid("param")
-        return c.json(await AppRuntime.runPromise(Workspace.Service.use((svc) => svc.remove(id))))
-      },
-    )
-    .post(
-      "/warp",
-      describeRoute({
-        summary: "Warp session into workspace",
-        description: "Move a session's sync history into the target workspace, or detach it to the local project.",
-        operationId: "experimental.workspace.warp",
-        responses: {
-          204: {
-            description: "Session warped",
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "json",
-        z.object({
-          id: zodObject(Workspace.Info).shape.id.nullable(),
-          sessionID: Workspace.SessionWarpInput.zodObject.shape.sessionID,
-          copyChanges: z.boolean().optional(),
-        }),
-      ),
-      async (c) => {
-        const body = c.req.valid("json")
-        return AppRuntime.runPromise(
-          Workspace.Service.use((workspace) =>
-            workspace.sessionWarp({
-              workspaceID: body.id,
-              sessionID: body.sessionID,
-              copyChanges: body.copyChanges,
-            }),
-          ).pipe(
-            Effect.match({
-              onFailure: (error) => {
-                if (error instanceof Vcs.PatchApplyError) {
-                  return c.json(
-                    {
-                      name: "VcsApplyError",
-                      data: {
-                        message: error.message,
-                        reason: error.reason,
-                      },
-                    },
-                    400,
-                  )
-                }
-                return c.json(
-                  {
-                    name: "WorkspaceWarpError",
-                    data: {
-                      message: error.message,
-                    },
-                  },
-                  400,
-                )
-              },
-              onSuccess: () => c.body(null, 204),
-            }),
-          ),
-        )
-      },
-    ),
-)

+ 0 - 286
packages/opencode/src/server/routes/global.ts

@@ -1,286 +0,0 @@
-import { Hono, type Context } from "hono"
-import { describeRoute, resolver, validator } from "hono-openapi"
-import { streamSSE } from "hono/streaming"
-import { Effect } from "effect"
-import z from "zod"
-import { BusEvent } from "@/bus/bus-event"
-import { SyncEvent } from "@/sync"
-import { GlobalBus } from "@/bus/global"
-import { Bus } from "@/bus"
-import { AppRuntime } from "@/effect/app-runtime"
-import { AsyncQueue } from "@/util/queue"
-import { Installation } from "@/installation"
-import { InstallationVersion } from "@opencode-ai/core/installation/version"
-import * as Log from "@opencode-ai/core/util/log"
-import { lazy } from "../../util/lazy"
-import { Config } from "@/config/config"
-import { errors } from "../error"
-import { disposeAllInstancesAndEmitGlobalDisposed } from "../global-lifecycle"
-
-const log = Log.create({ service: "server" })
-
-async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>) => () => void) {
-  return streamSSE(c, async (stream) => {
-    const q = new AsyncQueue<string | null>()
-    let done = false
-
-    q.push(
-      JSON.stringify({
-        payload: {
-          id: Bus.createID(),
-          type: "server.connected",
-          properties: {},
-        },
-      }),
-    )
-
-    // Send heartbeat every 10s to prevent stalled proxy streams.
-    const heartbeat = setInterval(() => {
-      q.push(
-        JSON.stringify({
-          payload: {
-            id: Bus.createID(),
-            type: "server.heartbeat",
-            properties: {},
-          },
-        }),
-      )
-    }, 10_000)
-
-    const stop = () => {
-      if (done) return
-      done = true
-      clearInterval(heartbeat)
-      unsub()
-      q.push(null)
-      log.info("global event disconnected")
-    }
-
-    const unsub = subscribe(q)
-
-    stream.onAbort(stop)
-
-    try {
-      for await (const data of q) {
-        if (data === null) return
-        await stream.writeSSE({ data })
-      }
-    } finally {
-      stop()
-    }
-  })
-}
-
-export const GlobalRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/health",
-      describeRoute({
-        summary: "Get health",
-        description: "Get health information about the OpenCode server.",
-        operationId: "global.health",
-        responses: {
-          200: {
-            description: "Health information",
-            content: {
-              "application/json": {
-                schema: resolver(z.object({ healthy: z.literal(true), version: z.string() })),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        return c.json({ healthy: true, version: InstallationVersion })
-      },
-    )
-    .get(
-      "/event",
-      describeRoute({
-        summary: "Get global events",
-        description: "Subscribe to global events from the OpenCode system using server-sent events.",
-        operationId: "global.event",
-        responses: {
-          200: {
-            description: "Event stream",
-            content: {
-              "text/event-stream": {
-                schema: resolver(
-                  z
-                    .object({
-                      directory: z.string(),
-                      project: z.string().optional(),
-                      workspace: z.string().optional(),
-                      payload: z.union([...BusEvent.payloads(), ...SyncEvent.payloads()]),
-                    })
-                    .meta({
-                      ref: "GlobalEvent",
-                    }),
-                ),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        log.info("global event connected")
-        c.header("Cache-Control", "no-cache, no-transform")
-        c.header("X-Accel-Buffering", "no")
-        c.header("X-Content-Type-Options", "nosniff")
-
-        return streamEvents(c, (q) => {
-          async function handler(event: any) {
-            q.push(JSON.stringify(event))
-          }
-          GlobalBus.on("event", handler)
-          return () => GlobalBus.off("event", handler)
-        })
-      },
-    )
-    .get(
-      "/config",
-      describeRoute({
-        summary: "Get global configuration",
-        description: "Retrieve the current global OpenCode configuration settings and preferences.",
-        operationId: "global.config.get",
-        responses: {
-          200: {
-            description: "Get global config info",
-            content: {
-              "application/json": {
-                schema: resolver(Config.Info.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        return c.json(await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())))
-      },
-    )
-    .patch(
-      "/config",
-      describeRoute({
-        summary: "Update global configuration",
-        description: "Update global OpenCode configuration settings and preferences.",
-        operationId: "global.config.update",
-        responses: {
-          200: {
-            description: "Successfully updated global config",
-            content: {
-              "application/json": {
-                schema: resolver(Config.Info.zod),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", Config.Info.zod),
-      async (c) => {
-        const config = c.req.valid("json")
-        const result = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.updateGlobal(config)))
-        if (result.changed) {
-          void AppRuntime.runPromise(disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true })).catch(
-            () => undefined,
-          )
-        }
-        return c.json(result.info)
-      },
-    )
-    .post(
-      "/dispose",
-      describeRoute({
-        summary: "Dispose instance",
-        description: "Clean up and dispose all OpenCode instances, releasing all resources.",
-        operationId: "global.dispose",
-        responses: {
-          200: {
-            description: "Global disposed",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        await AppRuntime.runPromise(disposeAllInstancesAndEmitGlobalDisposed())
-        return c.json(true)
-      },
-    )
-    .post(
-      "/upgrade",
-      describeRoute({
-        summary: "Upgrade opencode",
-        description: "Upgrade opencode to the specified version or latest if not specified.",
-        operationId: "global.upgrade",
-        responses: {
-          200: {
-            description: "Upgrade result",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z.union([
-                    z.object({
-                      success: z.literal(true),
-                      version: z.string(),
-                    }),
-                    z.object({
-                      success: z.literal(false),
-                      error: z.string(),
-                    }),
-                  ]),
-                ),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "json",
-        z.object({
-          target: z.string().optional(),
-        }),
-      ),
-      async (c) => {
-        const result = await AppRuntime.runPromise(
-          Installation.Service.use((svc) =>
-            Effect.gen(function* () {
-              const method = yield* svc.method()
-              if (method === "unknown") {
-                return { success: false as const, status: 400 as const, error: "Unknown installation method" }
-              }
-
-              const target = c.req.valid("json").target || (yield* svc.latest(method))
-              const result = yield* Effect.catch(
-                svc.upgrade(method, target).pipe(Effect.as({ success: true as const, version: target })),
-                (err) =>
-                  Effect.succeed({
-                    success: false as const,
-                    status: 500 as const,
-                    error: err instanceof Error ? err.message : String(err),
-                  }),
-              )
-              if (!result.success) return result
-              return { ...result, status: 200 as const }
-            }),
-          ),
-        )
-        if (!result.success) {
-          return c.json({ success: false, error: result.error }, result.status)
-        }
-        const target = result.version
-        GlobalBus.emit("event", {
-          directory: "global",
-          payload: {
-            type: Installation.Event.Updated.type,
-            properties: { version: target },
-          },
-        })
-        return c.json({ success: true, version: target })
-      },
-    ),
-)

+ 0 - 8
packages/opencode/src/server/routes/instance/AGENTS.md

@@ -1,8 +0,0 @@
-# Instance Route Parity
-
-This directory contains the legacy Hono instance routes and the experimental Effect HttpApi implementation under `httpapi/`. Keep them behaviorally aligned.
-
-- When adding, removing, or changing a legacy Hono route, update the matching Effect HttpApi group and handler in `httpapi/` in the same change unless the route is intentionally unsupported.
-- When changing an Effect HttpApi route, verify the legacy Hono route has the same public behavior, request shape, response shape, status codes, and instance/workspace routing semantics.
-- Keep OpenAPI/SDK-visible schemas aligned. If a difference is only an OpenAPI generation artifact, prefer fixing the source schema first; use `httpapi/public.ts` normalization only for compatibility shims that cannot be represented cleanly in the source schema.
-- Add or update parity coverage in `test/server/httpapi-bridge.test.ts` or the focused HttpApi tests when behavior or schema parity could regress.

+ 0 - 109
packages/opencode/src/server/routes/instance/config.ts

@@ -1,109 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import { Config } from "@/config/config"
-import { InstanceState } from "@/effect/instance-state"
-import { InstanceStore } from "@/project/instance-store"
-import { Provider } from "@/provider/provider"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { jsonRequest, runRequest } from "./trace"
-import { Effect } from "effect"
-import * as Log from "@opencode-ai/core/util/log"
-
-const log = Log.create({ service: "server.config" })
-
-export const ConfigRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/",
-      describeRoute({
-        summary: "Get configuration",
-        description: "Retrieve the current OpenCode configuration settings and preferences.",
-        operationId: "config.get",
-        responses: {
-          200: {
-            description: "Get config info",
-            content: {
-              "application/json": {
-                schema: resolver(Config.Info.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("ConfigRoutes.get", c, function* () {
-          const cfg = yield* Config.Service
-          return yield* cfg.get()
-        }),
-    )
-    .patch(
-      "/",
-      describeRoute({
-        summary: "Update configuration",
-        description: "Update OpenCode configuration settings and preferences.",
-        operationId: "config.update",
-        responses: {
-          200: {
-            description: "Successfully updated config",
-            content: {
-              "application/json": {
-                schema: resolver(Config.Info.zod),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", Config.Info.zod),
-      async (c) => {
-        const result = await runRequest(
-          "ConfigRoutes.update",
-          c,
-          Effect.gen(function* () {
-            const config = c.req.valid("json")
-            const cfg = yield* Config.Service
-            yield* cfg.update(config)
-            return { config, ctx: yield* InstanceState.context }
-          }),
-        )
-        const response = c.json(result.config)
-        void runRequest(
-          "ConfigRoutes.update.dispose",
-          c,
-          InstanceStore.Service.use((store) => store.dispose(result.ctx)).pipe(
-            Effect.uninterruptible,
-            Effect.catchCause((cause) => Effect.sync(() => log.warn("instance disposal failed", { cause }))),
-          ),
-        )
-        return response
-      },
-    )
-    .get(
-      "/providers",
-      describeRoute({
-        summary: "List config providers",
-        description: "Get a list of all configured AI providers and their default models.",
-        operationId: "config.providers",
-        responses: {
-          200: {
-            description: "List of providers",
-            content: {
-              "application/json": {
-                schema: resolver(Provider.ConfigProvidersResult.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("ConfigRoutes.providers", c, function* () {
-          const svc = yield* Provider.Service
-          const providers = yield* svc.list()
-          return {
-            providers: Object.values(providers),
-            default: Provider.defaultModelIDs(providers),
-          }
-        }),
-    ),
-)

+ 0 - 90
packages/opencode/src/server/routes/instance/event.ts

@@ -1,90 +0,0 @@
-import z from "zod"
-import { Hono } from "hono"
-import { describeRoute, resolver } from "hono-openapi"
-import { streamSSE } from "hono/streaming"
-import * as Log from "@opencode-ai/core/util/log"
-import { BusEvent } from "@/bus/bus-event"
-import { Bus } from "@/bus"
-import { AsyncQueue } from "@/util/queue"
-
-const log = Log.create({ service: "server" })
-
-export const EventRoutes = () =>
-  new Hono().get(
-    "/event",
-    describeRoute({
-      summary: "Subscribe to events",
-      description: "Get events",
-      operationId: "event.subscribe",
-      responses: {
-        200: {
-          description: "Event stream",
-          content: {
-            "text/event-stream": {
-              schema: resolver(
-                z.union(BusEvent.payloads()).meta({
-                  ref: "Event",
-                }),
-              ),
-            },
-          },
-        },
-      },
-    }),
-    async (c) => {
-      log.info("event connected")
-      c.header("Cache-Control", "no-cache, no-transform")
-      c.header("X-Accel-Buffering", "no")
-      c.header("X-Content-Type-Options", "nosniff")
-      return streamSSE(c, async (stream) => {
-        const q = new AsyncQueue<string | null>()
-        let done = false
-
-        q.push(
-          JSON.stringify({
-            id: Bus.createID(),
-            type: "server.connected",
-            properties: {},
-          }),
-        )
-
-        // Send heartbeat every 10s to prevent stalled proxy streams.
-        const heartbeat = setInterval(() => {
-          q.push(
-            JSON.stringify({
-              id: Bus.createID(),
-              type: "server.heartbeat",
-              properties: {},
-            }),
-          )
-        }, 10_000)
-
-        const stop = () => {
-          if (done) return
-          done = true
-          clearInterval(heartbeat)
-          unsub()
-          q.push(null)
-          log.info("event disconnected")
-        }
-
-        const unsub = Bus.subscribeAll((event) => {
-          q.push(JSON.stringify(event))
-          if (event.type === Bus.InstanceDisposed.type) {
-            stop()
-          }
-        })
-
-        stream.onAbort(stop)
-
-        try {
-          for await (const data of q) {
-            if (data === null) return
-            await stream.writeSSE({ data })
-          }
-        } finally {
-          stop()
-        }
-      })
-    },
-  )

+ 0 - 419
packages/opencode/src/server/routes/instance/experimental.ts

@@ -1,419 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import z from "zod"
-import * as EffectZod from "@/util/effect-zod"
-import { ProviderID, ModelID } from "@/provider/schema"
-import { ToolRegistry } from "@/tool/registry"
-import { Worktree } from "@/worktree"
-import { Instance } from "@/project/instance"
-import { Project } from "@/project/project"
-import { MCP } from "@/mcp"
-import { Session } from "@/session/session"
-import { Config } from "@/config/config"
-import { ConsoleState } from "@/config/console-state"
-import { Account } from "@/account/account"
-import { AccountID, OrgID } from "@/account/schema"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { Effect, Option } from "effect"
-import { Agent } from "@/agent/agent"
-import { jsonRequest, runRequest } from "./trace"
-
-const ConsoleOrgOption = z.object({
-  accountID: z.string(),
-  accountEmail: z.string(),
-  accountUrl: z.string(),
-  orgID: z.string(),
-  orgName: z.string(),
-  active: z.boolean(),
-})
-
-const ConsoleOrgList = z.object({
-  orgs: z.array(ConsoleOrgOption),
-})
-
-const ConsoleSwitchBody = z.object({
-  accountID: z.string(),
-  orgID: z.string(),
-})
-
-const QueryBoolean = z.union([
-  z.preprocess((value) => (value === "true" ? true : value === "false" ? false : value), z.boolean()),
-  z.enum(["true", "false"]),
-])
-
-function queryBoolean(value: z.infer<typeof QueryBoolean> | undefined) {
-  if (value === undefined) return
-  return value === true || value === "true"
-}
-
-export const ExperimentalRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/console",
-      describeRoute({
-        summary: "Get active Console provider metadata",
-        description: "Get the active Console org name and the set of provider IDs managed by that Console org.",
-        operationId: "experimental.console.get",
-        responses: {
-          200: {
-            description: "Active Console provider metadata",
-            content: {
-              "application/json": {
-                schema: resolver(ConsoleState.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.console.get", c, function* () {
-          const config = yield* Config.Service
-          const account = yield* Account.Service
-          const [state, groups] = yield* Effect.all([config.getConsoleState(), account.orgsByAccount()], {
-            concurrency: "unbounded",
-          })
-          return {
-            ...state,
-            switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
-          }
-        }),
-    )
-    .get(
-      "/console/orgs",
-      describeRoute({
-        summary: "List switchable Console orgs",
-        description: "Get the available Console orgs across logged-in accounts, including the current active org.",
-        operationId: "experimental.console.listOrgs",
-        responses: {
-          200: {
-            description: "Switchable Console orgs",
-            content: {
-              "application/json": {
-                schema: resolver(ConsoleOrgList),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.console.listOrgs", c, function* () {
-          const account = yield* Account.Service
-          const [groups, active] = yield* Effect.all([account.orgsByAccount(), account.active()], {
-            concurrency: "unbounded",
-          })
-          const info = Option.getOrUndefined(active)
-          const orgs = groups.flatMap((group) =>
-            group.orgs.map((org) => ({
-              accountID: group.account.id,
-              accountEmail: group.account.email,
-              accountUrl: group.account.url,
-              orgID: org.id,
-              orgName: org.name,
-              active: !!info && info.id === group.account.id && info.active_org_id === org.id,
-            })),
-          )
-          return { orgs }
-        }),
-    )
-    .post(
-      "/console/switch",
-      describeRoute({
-        summary: "Switch active Console org",
-        description: "Persist a new active Console account/org selection for the current local OpenCode state.",
-        operationId: "experimental.console.switchOrg",
-        responses: {
-          200: {
-            description: "Switch success",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      validator("json", ConsoleSwitchBody),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.console.switchOrg", c, function* () {
-          const body = c.req.valid("json")
-          const account = yield* Account.Service
-          yield* account.use(AccountID.make(body.accountID), Option.some(OrgID.make(body.orgID)))
-          return true
-        }),
-    )
-    .get(
-      "/tool/ids",
-      describeRoute({
-        summary: "List tool IDs",
-        description:
-          "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
-        operationId: "tool.ids",
-        responses: {
-          200: {
-            description: "Tool IDs",
-            content: {
-              "application/json": {
-                schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.tool.ids", c, function* () {
-          const registry = yield* ToolRegistry.Service
-          return yield* registry.ids()
-        }),
-    )
-    .get(
-      "/tool",
-      describeRoute({
-        summary: "List tools",
-        description:
-          "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
-        operationId: "tool.list",
-        responses: {
-          200: {
-            description: "Tools",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z
-                    .array(
-                      z
-                        .object({
-                          id: z.string(),
-                          description: z.string(),
-                          parameters: z.any(),
-                        })
-                        .meta({ ref: "ToolListItem" }),
-                    )
-                    .meta({ ref: "ToolList" }),
-                ),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          provider: z.string(),
-          model: z.string(),
-        }),
-      ),
-      async (c) => {
-        const { provider, model } = c.req.valid("query")
-        const tools = await runRequest(
-          "ExperimentalRoutes.tool.list",
-          c,
-          Effect.gen(function* () {
-            const agents = yield* Agent.Service
-            const registry = yield* ToolRegistry.Service
-            return yield* registry.tools({
-              providerID: ProviderID.make(provider),
-              modelID: ModelID.make(model),
-              agent: yield* agents.get(yield* agents.defaultAgent()),
-            })
-          }),
-        )
-        return c.json(
-          tools.map((t) => ({
-            id: t.id,
-            description: t.description,
-            parameters: EffectZod.toJsonSchema(t.parameters),
-          })),
-        )
-      },
-    )
-    .post(
-      "/worktree",
-      describeRoute({
-        summary: "Create worktree",
-        description: "Create a new git worktree for the current project and run any configured startup scripts.",
-        operationId: "worktree.create",
-        responses: {
-          200: {
-            description: "Worktree created",
-            content: {
-              "application/json": {
-                schema: resolver(Worktree.Info.zod),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", Worktree.CreateInput.zod.optional()),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.worktree.create", c, function* () {
-          const body = c.req.valid("json")
-          const svc = yield* Worktree.Service
-          return yield* svc.create(body)
-        }),
-    )
-    .get(
-      "/worktree",
-      describeRoute({
-        summary: "List worktrees",
-        description: "List all sandbox worktrees for the current project.",
-        operationId: "worktree.list",
-        responses: {
-          200: {
-            description: "List of worktree directories",
-            content: {
-              "application/json": {
-                schema: resolver(z.array(z.string())),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.worktree.list", c, function* () {
-          const svc = yield* Project.Service
-          return yield* svc.sandboxes(Instance.project.id)
-        }),
-    )
-    .delete(
-      "/worktree",
-      describeRoute({
-        summary: "Remove worktree",
-        description: "Remove a git worktree and delete its branch.",
-        operationId: "worktree.remove",
-        responses: {
-          200: {
-            description: "Worktree removed",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", Worktree.RemoveInput.zod),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.worktree.remove", c, function* () {
-          const body = c.req.valid("json")
-          const worktree = yield* Worktree.Service
-          const project = yield* Project.Service
-          yield* worktree.remove(body)
-          yield* project.removeSandbox(Instance.project.id, body.directory)
-          return true
-        }),
-    )
-    .post(
-      "/worktree/reset",
-      describeRoute({
-        summary: "Reset worktree",
-        description: "Reset a worktree branch to the primary default branch.",
-        operationId: "worktree.reset",
-        responses: {
-          200: {
-            description: "Worktree reset",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", Worktree.ResetInput.zod),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.worktree.reset", c, function* () {
-          const body = c.req.valid("json")
-          const svc = yield* Worktree.Service
-          yield* svc.reset(body)
-          return true
-        }),
-    )
-    .get(
-      "/session",
-      describeRoute({
-        summary: "List sessions",
-        description:
-          "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.",
-        operationId: "experimental.session.list",
-        responses: {
-          200: {
-            description: "List of sessions",
-            content: {
-              "application/json": {
-                schema: resolver(Session.GlobalInfo.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          directory: z.string().optional().meta({ description: "Filter sessions by project directory" }),
-          roots: QueryBoolean.optional().meta({ description: "Only return root sessions (no parentID)" }),
-          start: z.coerce
-            .number()
-            .optional()
-            .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
-          cursor: z.coerce
-            .number()
-            .optional()
-            .meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }),
-          search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
-          limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
-          archived: QueryBoolean.optional().meta({ description: "Include archived sessions (default false)" }),
-        }),
-      ),
-      async (c) => {
-        const query = c.req.valid("query")
-        const limit = query.limit ?? 100
-        const sessions: Session.GlobalInfo[] = []
-        for await (const session of Session.listGlobal({
-          directory: query.directory,
-          roots: queryBoolean(query.roots),
-          start: query.start,
-          cursor: query.cursor,
-          search: query.search,
-          limit: limit + 1,
-          archived: queryBoolean(query.archived),
-        })) {
-          sessions.push(session)
-        }
-        const hasMore = sessions.length > limit
-        const list = hasMore ? sessions.slice(0, limit) : sessions
-        if (hasMore && list.length > 0) {
-          c.header("x-next-cursor", String(list[list.length - 1].time.updated))
-        }
-        return c.json(list)
-      },
-    )
-    .get(
-      "/resource",
-      describeRoute({
-        summary: "Get MCP resources",
-        description: "Get all available MCP resources from connected servers. Optionally filter by name.",
-        operationId: "experimental.resource.list",
-        responses: {
-          200: {
-            description: "MCP resources",
-            content: {
-              "application/json": {
-                schema: resolver(z.record(z.string(), MCP.Resource.zod)),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("ExperimentalRoutes.resource.list", c, function* () {
-          const mcp = yield* MCP.Service
-          return yield* mcp.resources()
-        }),
-    ),
-)

+ 0 - 190
packages/opencode/src/server/routes/instance/file.ts

@@ -1,190 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import z from "zod"
-import { File } from "@/file"
-import { Ripgrep } from "@/file/ripgrep"
-import { LSP } from "@/lsp/lsp"
-import { Instance } from "@/project/instance"
-import { lazy } from "@/util/lazy"
-import { jsonRequest } from "./trace"
-
-export const FileRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/find",
-      describeRoute({
-        summary: "Find text",
-        description: "Search for text patterns across files in the project using ripgrep.",
-        operationId: "find.text",
-        responses: {
-          200: {
-            description: "Matches",
-            content: {
-              "application/json": {
-                schema: resolver(Ripgrep.SearchMatch.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          pattern: z.string(),
-        }),
-      ),
-      async (c) =>
-        jsonRequest("FileRoutes.findText", c, function* () {
-          const pattern = c.req.valid("query").pattern
-          const svc = yield* Ripgrep.Service
-          const result = yield* svc.search({ cwd: Instance.directory, pattern, limit: 10 })
-          return result.items
-        }),
-    )
-    .get(
-      "/find/file",
-      describeRoute({
-        summary: "Find files",
-        description: "Search for files or directories by name or pattern in the project directory.",
-        operationId: "find.files",
-        responses: {
-          200: {
-            description: "File paths",
-            content: {
-              "application/json": {
-                schema: resolver(z.string().array()),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          query: z.string(),
-          dirs: z.enum(["true", "false"]).optional(),
-          type: z.enum(["file", "directory"]).optional(),
-          limit: z.coerce.number().int().min(1).max(200).optional(),
-        }),
-      ),
-      async (c) =>
-        jsonRequest("FileRoutes.findFile", c, function* () {
-          const query = c.req.valid("query")
-          const svc = yield* File.Service
-          return yield* svc.search({
-            query: query.query,
-            limit: query.limit ?? 10,
-            dirs: query.dirs !== "false",
-            type: query.type,
-          })
-        }),
-    )
-    .get(
-      "/find/symbol",
-      describeRoute({
-        summary: "Find symbols",
-        description: "Search for workspace symbols like functions, classes, and variables using LSP.",
-        operationId: "find.symbols",
-        responses: {
-          200: {
-            description: "Symbols",
-            content: {
-              "application/json": {
-                schema: resolver(LSP.Symbol.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          query: z.string(),
-        }),
-      ),
-      async (c) => {
-        return c.json([])
-      },
-    )
-    .get(
-      "/file",
-      describeRoute({
-        summary: "List files",
-        description: "List files and directories in a specified path.",
-        operationId: "file.list",
-        responses: {
-          200: {
-            description: "Files and directories",
-            content: {
-              "application/json": {
-                schema: resolver(File.Node.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          path: z.string(),
-        }),
-      ),
-      async (c) =>
-        jsonRequest("FileRoutes.list", c, function* () {
-          const svc = yield* File.Service
-          return yield* svc.list(c.req.valid("query").path)
-        }),
-    )
-    .get(
-      "/file/content",
-      describeRoute({
-        summary: "Read file",
-        description: "Read the content of a specified file.",
-        operationId: "file.read",
-        responses: {
-          200: {
-            description: "File content",
-            content: {
-              "application/json": {
-                schema: resolver(File.Content.zod),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          path: z.string(),
-        }),
-      ),
-      async (c) =>
-        jsonRequest("FileRoutes.read", c, function* () {
-          const svc = yield* File.Service
-          return yield* svc.read(c.req.valid("query").path)
-        }),
-    )
-    .get(
-      "/file/status",
-      describeRoute({
-        summary: "Get file status",
-        description: "Get the git status of all files in the project.",
-        operationId: "file.status",
-        responses: {
-          200: {
-            description: "File status",
-            content: {
-              "application/json": {
-                schema: resolver(File.Info.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("FileRoutes.status", c, function* () {
-          const svc = yield* File.Service
-          return yield* svc.status()
-        }),
-    ),
-)

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

@@ -1,4 +1,5 @@
 import { TuiEvent } from "@/cli/cmd/tui/event"
+import { TuiRequest as TuiRequestPayload } from "@/server/shared/tui-control"
 import { Schema } from "effect"
 import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 import { Authorization } from "../middleware/authorization"
@@ -9,10 +10,6 @@ import { described } from "./metadata"
 
 const root = "/tui"
 export const CommandPayload = Schema.Struct({ command: Schema.String })
-export const TuiRequestPayload = Schema.Struct({
-  path: Schema.String,
-  body: Schema.Unknown,
-})
 const EventTuiPromptAppend = Schema.Struct({
   type: Schema.Literal(TuiEvent.PromptAppend.type),
   properties: TuiEvent.PromptAppend.properties,

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

@@ -80,7 +80,6 @@ import { instanceContextLayer, instanceRouterMiddleware } from "./middleware/ins
 import { workspaceRouterMiddleware, workspaceRoutingLayer } from "./middleware/workspace-routing"
 import { disposeMiddleware } from "./lifecycle"
 import { memoMap } from "@opencode-ai/core/effect/memo-map"
-import * as ServerBackend from "@/server/backend"
 import { compressionLayer } from "./middleware/compression"
 import { corsVaryFix } from "./middleware/cors-vary"
 import { errorLayer } from "./middleware/error"
@@ -91,8 +90,7 @@ export const context = Context.makeUnsafe<unknown>(new Map())
 const runtime = HttpRouter.middleware()(
   Effect.succeed((effect) =>
     Effect.gen(function* () {
-      const selected = ServerBackend.select()
-      yield* Effect.annotateCurrentSpan(ServerBackend.attributes(ServerBackend.force(selected, "effect-httpapi")))
+      yield* Effect.annotateCurrentSpan({ "opencode.server.backend": "effect-httpapi" })
       return yield* effect
     }),
   ),

+ 0 - 502
packages/opencode/src/server/routes/instance/index.ts

@@ -1,502 +0,0 @@
-import { describeRoute, resolver, validator } from "hono-openapi"
-import { Hono } from "hono"
-import type { UpgradeWebSocket } from "hono/ws"
-import { Context, Effect } from "effect"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import z from "zod"
-import { Format } from "@/format"
-import { TuiRoutes } from "./tui"
-import { Instance } from "@/project/instance"
-import { InstanceRuntime } from "@/project/instance-runtime"
-import { Vcs } from "@/project/vcs"
-import { Agent } from "@/agent/agent"
-import { Skill } from "@/skill"
-import { Global } from "@opencode-ai/core/global"
-import { LSP } from "@/lsp/lsp"
-import { Command } from "@/command"
-import { QuestionRoutes } from "./question"
-import { PermissionRoutes } from "./permission"
-import { ProjectRoutes } from "./project"
-import { SessionRoutes } from "./session"
-import { PtyRoutes } from "./pty"
-import { McpRoutes } from "./mcp"
-import { FileRoutes } from "./file"
-import { ConfigRoutes } from "./config"
-import { ExperimentalRoutes } from "./experimental"
-import { ProviderRoutes } from "./provider"
-import { EventRoutes } from "./event"
-import { SyncRoutes } from "./sync"
-import { InstanceMiddleware } from "./middleware"
-import { jsonRequest, runRequest } from "./trace"
-import { ExperimentalHttpApiServer } from "./httpapi/server"
-import { EventPaths } from "./httpapi/event"
-import { ExperimentalPaths } from "./httpapi/groups/experimental"
-import { FilePaths } from "./httpapi/groups/file"
-import { InstancePaths } from "./httpapi/groups/instance"
-import { McpPaths } from "./httpapi/groups/mcp"
-import { PtyPaths } from "./httpapi/groups/pty"
-import { SessionPaths } from "./httpapi/groups/session"
-import { SyncPaths } from "./httpapi/groups/sync"
-import { TuiPaths } from "./httpapi/groups/tui"
-import { WorkspacePaths } from "./httpapi/groups/workspace"
-import type { CorsOptions } from "@/server/cors"
-import { errors } from "@/server/error"
-
-export const InstanceRoutes = (upgrade: UpgradeWebSocket, opts?: CorsOptions): Hono => {
-  const app = new Hono()
-  const handler = ExperimentalHttpApiServer.webHandler(opts).handler
-  const context = Context.empty() as Context.Context<unknown>
-
-  app.all("/api/*", (c) => handler(c.req.raw, context))
-
-  if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
-    app.get(EventPaths.event, (c) => handler(c.req.raw, context))
-    app.get("/question", (c) => handler(c.req.raw, context))
-    app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context))
-    app.post("/question/:requestID/reject", (c) => handler(c.req.raw, context))
-    app.get("/permission", (c) => handler(c.req.raw, context))
-    app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context))
-    app.get("/config", (c) => handler(c.req.raw, context))
-    app.patch("/config", (c) => handler(c.req.raw, context))
-    app.get("/config/providers", (c) => handler(c.req.raw, context))
-    app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context))
-    app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context))
-    app.post(ExperimentalPaths.consoleSwitch, (c) => handler(c.req.raw, context))
-    app.get(ExperimentalPaths.tool, (c) => handler(c.req.raw, context))
-    app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context))
-    app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
-    app.post(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
-    app.delete(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
-    app.post(ExperimentalPaths.worktreeReset, (c) => handler(c.req.raw, context))
-    app.get(ExperimentalPaths.session, (c) => handler(c.req.raw, context))
-    app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context))
-    app.get("/provider", (c) => handler(c.req.raw, context))
-    app.get("/provider/auth", (c) => handler(c.req.raw, context))
-    app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context))
-    app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context))
-    app.get("/project", (c) => handler(c.req.raw, context))
-    app.get("/project/current", (c) => handler(c.req.raw, context))
-    app.post("/project/git/init", (c) => handler(c.req.raw, context))
-    app.patch("/project/:projectID", (c) => handler(c.req.raw, context))
-    app.get(FilePaths.findText, (c) => handler(c.req.raw, context))
-    app.get(FilePaths.findFile, (c) => handler(c.req.raw, context))
-    app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context))
-    app.get(FilePaths.list, (c) => handler(c.req.raw, context))
-    app.get(FilePaths.content, (c) => handler(c.req.raw, context))
-    app.get(FilePaths.status, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.path, (c) => handler(c.req.raw, context))
-    app.post(InstancePaths.dispose, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.vcsStatus, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.vcsDiffRaw, (c) => handler(c.req.raw, context))
-    app.post(InstancePaths.vcsApply, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.command, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.agent, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.skill, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.lsp, (c) => handler(c.req.raw, context))
-    app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context))
-    app.get(McpPaths.status, (c) => handler(c.req.raw, context))
-    app.post(McpPaths.status, (c) => handler(c.req.raw, context))
-    app.post(McpPaths.auth, (c) => handler(c.req.raw, context))
-    app.post(McpPaths.authCallback, (c) => handler(c.req.raw, context))
-    app.post(McpPaths.authAuthenticate, (c) => handler(c.req.raw, context))
-    app.delete(McpPaths.auth, (c) => handler(c.req.raw, context))
-    app.post(McpPaths.connect, (c) => handler(c.req.raw, context))
-    app.post(McpPaths.disconnect, (c) => handler(c.req.raw, context))
-    app.post(SyncPaths.start, (c) => handler(c.req.raw, context))
-    app.post(SyncPaths.replay, (c) => handler(c.req.raw, context))
-    app.post(SyncPaths.history, (c) => handler(c.req.raw, context))
-    app.get(PtyPaths.list, (c) => handler(c.req.raw, context))
-    app.post(PtyPaths.create, (c) => handler(c.req.raw, context))
-    app.get(PtyPaths.get, (c) => handler(c.req.raw, context))
-    app.put(PtyPaths.update, (c) => handler(c.req.raw, context))
-    app.delete(PtyPaths.remove, (c) => handler(c.req.raw, context))
-    app.post(PtyPaths.connectToken, (c) => handler(c.req.raw, context))
-    app.get(PtyPaths.connect, (c) => handler(c.req.raw, context))
-    app.get(SessionPaths.list, (c) => handler(c.req.raw, context))
-    app.get(SessionPaths.status, (c) => handler(c.req.raw, context))
-    app.get(SessionPaths.get, (c) => handler(c.req.raw, context))
-    app.get(SessionPaths.children, (c) => handler(c.req.raw, context))
-    app.get(SessionPaths.todo, (c) => handler(c.req.raw, context))
-    app.get(SessionPaths.diff, (c) => handler(c.req.raw, context))
-    app.get(SessionPaths.messages, (c) => handler(c.req.raw, context))
-    app.get(SessionPaths.message, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.create, (c) => handler(c.req.raw, context))
-    app.delete(SessionPaths.remove, (c) => handler(c.req.raw, context))
-    app.patch(SessionPaths.update, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.init, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.fork, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.abort, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.share, (c) => handler(c.req.raw, context))
-    app.delete(SessionPaths.share, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.summarize, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.prompt, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.promptAsync, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.command, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.shell, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.revert, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.unrevert, (c) => handler(c.req.raw, context))
-    app.post(SessionPaths.permissions, (c) => handler(c.req.raw, context))
-    app.delete(SessionPaths.deleteMessage, (c) => handler(c.req.raw, context))
-    app.delete(SessionPaths.deletePart, (c) => handler(c.req.raw, context))
-    app.patch(SessionPaths.updatePart, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.appendPrompt, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.openHelp, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.openSessions, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.openThemes, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.openModels, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.submitPrompt, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.clearPrompt, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.executeCommand, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.showToast, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.publish, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.selectSession, (c) => handler(c.req.raw, context))
-    app.get(TuiPaths.controlNext, (c) => handler(c.req.raw, context))
-    app.post(TuiPaths.controlResponse, (c) => handler(c.req.raw, context))
-    app.get(WorkspacePaths.adapters, (c) => handler(c.req.raw, context))
-    app.post(WorkspacePaths.list, (c) => handler(c.req.raw, context))
-    app.get(WorkspacePaths.list, (c) => handler(c.req.raw, context))
-    app.get(WorkspacePaths.status, (c) => handler(c.req.raw, context))
-    app.delete(WorkspacePaths.remove, (c) => handler(c.req.raw, context))
-    app.post(WorkspacePaths.warp, (c) => handler(c.req.raw, context))
-  }
-
-  return app
-    .route("/project", ProjectRoutes())
-    .route("/pty", PtyRoutes(upgrade, opts))
-    .route("/config", ConfigRoutes())
-    .route("/experimental", ExperimentalRoutes())
-    .route("/session", SessionRoutes())
-    .route("/permission", PermissionRoutes())
-    .route("/question", QuestionRoutes())
-    .route("/provider", ProviderRoutes())
-    .route("/sync", SyncRoutes())
-    .route("/", FileRoutes())
-    .route("/", EventRoutes())
-    .route("/mcp", McpRoutes())
-    .route("/tui", TuiRoutes())
-    .post(
-      "/instance/dispose",
-      describeRoute({
-        summary: "Dispose instance",
-        description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
-        operationId: "instance.dispose",
-        responses: {
-          200: {
-            description: "Instance disposed",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        await InstanceRuntime.disposeInstance(Instance.current)
-        return c.json(true)
-      },
-    )
-    .get(
-      "/path",
-      describeRoute({
-        summary: "Get paths",
-        description: "Retrieve the current working directory and related path information for the OpenCode instance.",
-        operationId: "path.get",
-        responses: {
-          200: {
-            description: "Path",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z
-                    .object({
-                      home: z.string(),
-                      state: z.string(),
-                      config: z.string(),
-                      worktree: z.string(),
-                      directory: z.string(),
-                    })
-                    .meta({
-                      ref: "Path",
-                    }),
-                ),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        return c.json({
-          home: Global.Path.home,
-          state: Global.Path.state,
-          config: Global.Path.config,
-          worktree: Instance.worktree,
-          directory: Instance.directory,
-        })
-      },
-    )
-    .get(
-      "/vcs",
-      describeRoute({
-        summary: "Get VCS info",
-        description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
-        operationId: "vcs.get",
-        responses: {
-          200: {
-            description: "VCS info",
-            content: {
-              "application/json": {
-                schema: resolver(Vcs.Info.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("InstanceRoutes.vcs.get", c, function* () {
-          const vcs = yield* Vcs.Service
-          const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], {
-            concurrency: 2,
-          })
-          return { branch, default_branch }
-        }),
-    )
-    .get(
-      "/vcs/diff",
-      describeRoute({
-        summary: "Get VCS diff",
-        description: "Retrieve the current git diff for the working tree or against the default branch.",
-        operationId: "vcs.diff",
-        responses: {
-          200: {
-            description: "VCS diff",
-            content: {
-              "application/json": {
-                schema: resolver(Vcs.FileDiff.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          mode: Vcs.Mode.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("InstanceRoutes.vcs.diff", c, function* () {
-          const vcs = yield* Vcs.Service
-          return yield* vcs.diff(c.req.valid("query").mode)
-        }),
-    )
-    .get(
-      "/vcs/status",
-      describeRoute({
-        summary: "Get VCS status",
-        description: "Retrieve changed files in the current working tree without patches.",
-        operationId: "vcs.status",
-        responses: {
-          200: {
-            description: "VCS status",
-            content: {
-              "application/json": {
-                schema: resolver(Vcs.FileStatus.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("InstanceRoutes.vcs.status", c, function* () {
-          const vcs = yield* Vcs.Service
-          return yield* vcs.status()
-        }),
-    )
-    .get(
-      "/vcs/diff/raw",
-      describeRoute({
-        summary: "Get raw VCS diff",
-        description: "Retrieve a raw patch for current uncommitted changes.",
-        operationId: "vcs.diff.raw",
-        responses: {
-          200: {
-            description: "Raw VCS diff",
-            content: {
-              "text/x-diff": {
-                schema: resolver(z.string()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        const patch = await runRequest(
-          "InstanceRoutes.vcs.diffRaw",
-          c,
-          Vcs.Service.use((vcs) => vcs.diffRaw()),
-        )
-        return c.text(patch, 200, { "content-type": "text/x-diff; charset=utf-8" })
-      },
-    )
-    .post(
-      "/vcs/apply",
-      describeRoute({
-        summary: "Apply VCS patch",
-        description: "Apply a raw patch to the current working tree.",
-        operationId: "vcs.apply",
-        responses: {
-          200: {
-            description: "VCS patch applied",
-            content: {
-              "application/json": {
-                schema: resolver(Vcs.ApplyResult.zod),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", Vcs.ApplyInput.zodObject),
-      async (c) => {
-        const result = await runRequest(
-          "InstanceRoutes.vcs.apply",
-          c,
-          Vcs.Service.use((vcs) => vcs.apply(c.req.valid("json") as Vcs.ApplyInput)).pipe(
-            Effect.match({
-              onFailure: (error) => ({ ok: false as const, error }),
-              onSuccess: (value) => ({ ok: true as const, value }),
-            }),
-          ),
-        )
-        if (result.ok) return c.json(result.value)
-        return c.json(
-          {
-            name: "VcsApplyError",
-            data: {
-              message: result.error.message,
-              reason: result.error.reason,
-            },
-          },
-          400,
-        )
-      },
-    )
-    .get(
-      "/command",
-      describeRoute({
-        summary: "List commands",
-        description: "Get a list of all available commands in the OpenCode system.",
-        operationId: "command.list",
-        responses: {
-          200: {
-            description: "List of commands",
-            content: {
-              "application/json": {
-                schema: resolver(Command.Info.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("InstanceRoutes.command.list", c, function* () {
-          const svc = yield* Command.Service
-          return yield* svc.list()
-        }),
-    )
-    .get(
-      "/agent",
-      describeRoute({
-        summary: "List agents",
-        description: "Get a list of all available AI agents in the OpenCode system.",
-        operationId: "app.agents",
-        responses: {
-          200: {
-            description: "List of agents",
-            content: {
-              "application/json": {
-                schema: resolver(Agent.Info.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("InstanceRoutes.agent.list", c, function* () {
-          const svc = yield* Agent.Service
-          return yield* svc.list()
-        }),
-    )
-    .get(
-      "/skill",
-      describeRoute({
-        summary: "List skills",
-        description: "Get a list of all available skills in the OpenCode system.",
-        operationId: "app.skills",
-        responses: {
-          200: {
-            description: "List of skills",
-            content: {
-              "application/json": {
-                schema: resolver(Skill.Info.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("InstanceRoutes.skill.list", c, function* () {
-          const skill = yield* Skill.Service
-          return yield* skill.all()
-        }),
-    )
-    .get(
-      "/lsp",
-      describeRoute({
-        summary: "Get LSP status",
-        description: "Get LSP server status",
-        operationId: "lsp.status",
-        responses: {
-          200: {
-            description: "LSP server status",
-            content: {
-              "application/json": {
-                schema: resolver(LSP.Status.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("InstanceRoutes.lsp.status", c, function* () {
-          const lsp = yield* LSP.Service
-          return yield* lsp.status()
-        }),
-    )
-    .get(
-      "/formatter",
-      describeRoute({
-        summary: "Get formatter status",
-        description: "Get formatter status",
-        operationId: "formatter.status",
-        responses: {
-          200: {
-            description: "Formatter status",
-            content: {
-              "application/json": {
-                schema: resolver(Format.Status.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("InstanceRoutes.formatter.status", c, function* () {
-          const svc = yield* Format.Service
-          return yield* svc.status()
-        }),
-    )
-}

+ 0 - 277
packages/opencode/src/server/routes/instance/mcp.ts

@@ -1,277 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import z from "zod"
-import { MCP } from "@/mcp"
-import { ConfigMCP } from "@/config/mcp"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { Effect } from "effect"
-import { jsonRequest, runRequest } from "./trace"
-
-const UnsupportedOAuthError = z
-  .object({
-    error: z.string(),
-  })
-  .meta({ ref: "McpUnsupportedOAuthError" })
-
-const unsupportedOAuthErrorResponse = {
-  description: "MCP server does not support OAuth",
-  content: {
-    "application/json": {
-      schema: resolver(UnsupportedOAuthError),
-    },
-  },
-}
-
-export const McpRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/",
-      describeRoute({
-        summary: "Get MCP status",
-        description: "Get the status of all Model Context Protocol (MCP) servers.",
-        operationId: "mcp.status",
-        responses: {
-          200: {
-            description: "MCP server status",
-            content: {
-              "application/json": {
-                schema: resolver(z.record(z.string(), MCP.Status.zod)),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("McpRoutes.status", c, function* () {
-          const mcp = yield* MCP.Service
-          return yield* mcp.status()
-        }),
-    )
-    .post(
-      "/",
-      describeRoute({
-        summary: "Add MCP server",
-        description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
-        operationId: "mcp.add",
-        responses: {
-          200: {
-            description: "MCP server added successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.record(z.string(), MCP.Status.zod)),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "json",
-        z.object({
-          name: z.string(),
-          config: ConfigMCP.Info.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("McpRoutes.add", c, function* () {
-          const { name, config } = c.req.valid("json")
-          const mcp = yield* MCP.Service
-          const result = yield* mcp.add(name, config)
-          return result.status
-        }),
-    )
-    .post(
-      "/:name/auth",
-      describeRoute({
-        summary: "Start MCP OAuth",
-        description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
-        operationId: "mcp.auth.start",
-        responses: {
-          200: {
-            description: "OAuth flow started",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z.object({
-                    authorizationUrl: z.string().describe("URL to open in browser for authorization"),
-                  }),
-                ),
-              },
-            },
-          },
-          400: unsupportedOAuthErrorResponse,
-          ...errors(404),
-        },
-      }),
-      async (c) => {
-        const name = c.req.param("name")
-        const result = await runRequest(
-          "McpRoutes.auth.start",
-          c,
-          Effect.gen(function* () {
-            const mcp = yield* MCP.Service
-            const supports = yield* mcp.supportsOAuth(name)
-            if (!supports) return { supports }
-            return {
-              supports,
-              auth: yield* mcp.startAuth(name),
-            }
-          }),
-        )
-        if (!result.supports) {
-          return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
-        }
-        return c.json(result.auth)
-      },
-    )
-    .post(
-      "/:name/auth/callback",
-      describeRoute({
-        summary: "Complete MCP OAuth",
-        description:
-          "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
-        operationId: "mcp.auth.callback",
-        responses: {
-          200: {
-            description: "OAuth authentication completed",
-            content: {
-              "application/json": {
-                schema: resolver(MCP.Status.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "json",
-        z.object({
-          code: z.string().describe("Authorization code from OAuth callback"),
-        }),
-      ),
-      async (c) =>
-        jsonRequest("McpRoutes.auth.callback", c, function* () {
-          const name = c.req.param("name")
-          const { code } = c.req.valid("json")
-          const mcp = yield* MCP.Service
-          return yield* mcp.finishAuth(name, code)
-        }),
-    )
-    .post(
-      "/:name/auth/authenticate",
-      describeRoute({
-        summary: "Authenticate MCP OAuth",
-        description: "Start OAuth flow and wait for callback (opens browser)",
-        operationId: "mcp.auth.authenticate",
-        responses: {
-          200: {
-            description: "OAuth authentication completed",
-            content: {
-              "application/json": {
-                schema: resolver(MCP.Status.zod),
-              },
-            },
-          },
-          400: unsupportedOAuthErrorResponse,
-          ...errors(404),
-        },
-      }),
-      async (c) => {
-        const name = c.req.param("name")
-        const result = await runRequest(
-          "McpRoutes.auth.authenticate",
-          c,
-          Effect.gen(function* () {
-            const mcp = yield* MCP.Service
-            const supports = yield* mcp.supportsOAuth(name)
-            if (!supports) return { supports }
-            return {
-              supports,
-              status: yield* mcp.authenticate(name),
-            }
-          }),
-        )
-        if (!result.supports) {
-          return c.json({ error: `MCP server ${name} does not support OAuth` }, 400)
-        }
-        return c.json(result.status)
-      },
-    )
-    .delete(
-      "/:name/auth",
-      describeRoute({
-        summary: "Remove MCP OAuth",
-        description: "Remove OAuth credentials for an MCP server",
-        operationId: "mcp.auth.remove",
-        responses: {
-          200: {
-            description: "OAuth credentials removed",
-            content: {
-              "application/json": {
-                schema: resolver(z.object({ success: z.literal(true) })),
-              },
-            },
-          },
-          ...errors(404),
-        },
-      }),
-      async (c) =>
-        jsonRequest("McpRoutes.auth.remove", c, function* () {
-          const name = c.req.param("name")
-          const mcp = yield* MCP.Service
-          yield* mcp.removeAuth(name)
-          return { success: true as const }
-        }),
-    )
-    .post(
-      "/:name/connect",
-      describeRoute({
-        description: "Connect an MCP server",
-        operationId: "mcp.connect",
-        responses: {
-          200: {
-            description: "MCP server connected successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      validator("param", z.object({ name: z.string() })),
-      async (c) =>
-        jsonRequest("McpRoutes.connect", c, function* () {
-          const { name } = c.req.valid("param")
-          const mcp = yield* MCP.Service
-          yield* mcp.connect(name)
-          return true
-        }),
-    )
-    .post(
-      "/:name/disconnect",
-      describeRoute({
-        description: "Disconnect an MCP server",
-        operationId: "mcp.disconnect",
-        responses: {
-          200: {
-            description: "MCP server disconnected successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      validator("param", z.object({ name: z.string() })),
-      async (c) =>
-        jsonRequest("McpRoutes.disconnect", c, function* () {
-          const { name } = c.req.valid("param")
-          const mcp = yield* MCP.Service
-          yield* mcp.disconnect(name)
-          return true
-        }),
-    ),
-)

+ 0 - 32
packages/opencode/src/server/routes/instance/middleware.ts

@@ -1,32 +0,0 @@
-import type { MiddlewareHandler } from "hono"
-import { WithInstance } from "@/project/with-instance"
-import { AppFileSystem } from "@opencode-ai/core/filesystem"
-import { WorkspaceContext } from "@/control-plane/workspace-context"
-import { WorkspaceID } from "@/control-plane/schema"
-
-export function InstanceMiddleware(workspaceID?: WorkspaceID): MiddlewareHandler {
-  return async (c, next) => {
-    const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
-    const directory = AppFileSystem.resolve(
-      (() => {
-        try {
-          return decodeURIComponent(raw)
-        } catch {
-          return raw
-        }
-      })(),
-    )
-
-    return WorkspaceContext.provide({
-      workspaceID,
-      async fn() {
-        return WithInstance.provide({
-          directory,
-          async fn() {
-            return next()
-          },
-        })
-      },
-    })
-  }
-}

+ 0 - 73
packages/opencode/src/server/routes/instance/permission.ts

@@ -1,73 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import z from "zod"
-import { Permission } from "@/permission"
-import { PermissionID } from "@/permission/schema"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { jsonRequest } from "./trace"
-
-export const PermissionRoutes = lazy(() =>
-  new Hono()
-    .post(
-      "/:requestID/reply",
-      describeRoute({
-        summary: "Respond to permission request",
-        description: "Approve or deny a permission request from the AI assistant.",
-        operationId: "permission.reply",
-        responses: {
-          200: {
-            description: "Permission processed successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          requestID: PermissionID.zod,
-        }),
-      ),
-      validator("json", z.object({ reply: Permission.Reply.zod, message: z.string().optional() })),
-      async (c) =>
-        jsonRequest("PermissionRoutes.reply", c, function* () {
-          const params = c.req.valid("param")
-          const json = c.req.valid("json")
-          const svc = yield* Permission.Service
-          yield* svc.reply({
-            requestID: params.requestID,
-            reply: json.reply,
-            message: json.message,
-          })
-          return true
-        }),
-    )
-    .get(
-      "/",
-      describeRoute({
-        summary: "List pending permissions",
-        description: "Get all pending permission requests across all sessions.",
-        operationId: "permission.list",
-        responses: {
-          200: {
-            description: "List of pending permissions",
-            content: {
-              "application/json": {
-                schema: resolver(Permission.Request.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("PermissionRoutes.list", c, function* () {
-          const svc = yield* Permission.Service
-          return yield* svc.list()
-        }),
-    ),
-)

+ 0 - 116
packages/opencode/src/server/routes/instance/project.ts

@@ -1,116 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, validator } from "hono-openapi"
-import { resolver } from "hono-openapi"
-import { Instance } from "@/project/instance"
-import { InstanceRuntime } from "@/project/instance-runtime"
-import { Project } from "@/project/project"
-import z from "zod"
-import { ProjectID } from "@/project/schema"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { jsonRequest, runRequest } from "./trace"
-
-export const ProjectRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/",
-      describeRoute({
-        summary: "List all projects",
-        description: "Get a list of projects that have been opened with OpenCode.",
-        operationId: "project.list",
-        responses: {
-          200: {
-            description: "List of projects",
-            content: {
-              "application/json": {
-                schema: resolver(Project.Info.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        const projects = Project.list()
-        return c.json(projects)
-      },
-    )
-    .get(
-      "/current",
-      describeRoute({
-        summary: "Get current project",
-        description: "Retrieve the currently active project that OpenCode is working with.",
-        operationId: "project.current",
-        responses: {
-          200: {
-            description: "Current project information",
-            content: {
-              "application/json": {
-                schema: resolver(Project.Info.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        return c.json(Instance.project)
-      },
-    )
-    .post(
-      "/git/init",
-      describeRoute({
-        summary: "Initialize git repository",
-        description: "Create a git repository for the current project and return the refreshed project info.",
-        operationId: "project.initGit",
-        responses: {
-          200: {
-            description: "Project information after git initialization",
-            content: {
-              "application/json": {
-                schema: resolver(Project.Info.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        const dir = Instance.directory
-        const prev = Instance.project
-        const next = await runRequest(
-          "ProjectRoutes.initGit",
-          c,
-          Project.Service.use((svc) => svc.initGit({ directory: dir, project: prev })),
-        )
-        if (next.id === prev.id && next.vcs === prev.vcs && next.worktree === prev.worktree) return c.json(next)
-        await InstanceRuntime.reloadInstance({ directory: dir, worktree: dir, project: next })
-        return c.json(next)
-      },
-    )
-    .patch(
-      "/:projectID",
-      describeRoute({
-        summary: "Update project",
-        description: "Update project properties such as name, icon, and commands.",
-        operationId: "project.update",
-        responses: {
-          200: {
-            description: "Updated project information",
-            content: {
-              "application/json": {
-                schema: resolver(Project.Info.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator("param", z.object({ projectID: ProjectID.zod })),
-      validator("json", Project.UpdateInput.omit({ projectID: true })),
-      async (c) =>
-        jsonRequest("ProjectRoutes.update", c, function* () {
-          const projectID = c.req.valid("param").projectID
-          const body = c.req.valid("json")
-          const svc = yield* Project.Service
-          return yield* svc.update({ ...body, projectID })
-        }),
-    ),
-)

+ 0 - 158
packages/opencode/src/server/routes/instance/provider.ts

@@ -1,158 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import z from "zod"
-import { Config } from "@/config/config"
-import { Provider } from "@/provider/provider"
-import { ModelsDev } from "@/provider/models"
-import { ProviderAuth } from "@/provider/auth"
-import { ProviderID } from "@/provider/schema"
-import { mapValues } from "remeda"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { Effect } from "effect"
-import { jsonRequest } from "./trace"
-
-export const ProviderRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/",
-      describeRoute({
-        summary: "List providers",
-        description: "Get a list of all available AI providers, including both available and connected ones.",
-        operationId: "provider.list",
-        responses: {
-          200: {
-            description: "List of providers",
-            content: {
-              "application/json": {
-                schema: resolver(Provider.ListResult.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("ProviderRoutes.list", c, function* () {
-          const svc = yield* Provider.Service
-          const cfg = yield* Config.Service
-          const config = yield* cfg.get()
-          const all = yield* ModelsDev.Service.use((s) => s.get())
-          const disabled = new Set(config.disabled_providers ?? [])
-          const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
-          const filtered: Record<string, (typeof all)[string]> = {}
-          for (const [key, value] of Object.entries(all)) {
-            if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) {
-              filtered[key] = value
-            }
-          }
-          const connected = yield* svc.list()
-          const providers = Object.assign(
-            mapValues(filtered, (x) => Provider.fromModelsDevProvider(x)),
-            connected,
-          )
-          return {
-            all: Object.values(providers),
-            default: Provider.defaultModelIDs(providers),
-            connected: Object.keys(connected),
-          }
-        }),
-    )
-    .get(
-      "/auth",
-      describeRoute({
-        summary: "Get provider auth methods",
-        description: "Retrieve available authentication methods for all AI providers.",
-        operationId: "provider.auth",
-        responses: {
-          200: {
-            description: "Provider auth methods",
-            content: {
-              "application/json": {
-                schema: resolver(ProviderAuth.Methods.zod),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("ProviderRoutes.auth", c, function* () {
-          const svc = yield* ProviderAuth.Service
-          return yield* svc.methods()
-        }),
-    )
-    .post(
-      "/:providerID/oauth/authorize",
-      describeRoute({
-        summary: "OAuth authorize",
-        description: "Initiate OAuth authorization for a specific AI provider to get an authorization URL.",
-        operationId: "provider.oauth.authorize",
-        responses: {
-          200: {
-            description: "Authorization URL and method",
-            content: {
-              "application/json": {
-                schema: resolver(ProviderAuth.Authorization.zod.optional()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          providerID: ProviderID.zod.meta({ description: "Provider ID" }),
-        }),
-      ),
-      validator("json", ProviderAuth.AuthorizeInput.zod),
-      async (c) =>
-        jsonRequest("ProviderRoutes.oauth.authorize", c, function* () {
-          const providerID = c.req.valid("param").providerID
-          const { method, inputs } = c.req.valid("json")
-          const svc = yield* ProviderAuth.Service
-          return yield* svc.authorize({
-            providerID,
-            method,
-            inputs,
-          })
-        }),
-    )
-    .post(
-      "/:providerID/oauth/callback",
-      describeRoute({
-        summary: "OAuth callback",
-        description: "Handle the OAuth callback from a provider after user authorization.",
-        operationId: "provider.oauth.callback",
-        responses: {
-          200: {
-            description: "OAuth callback processed successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          providerID: ProviderID.zod.meta({ description: "Provider ID" }),
-        }),
-      ),
-      validator("json", ProviderAuth.CallbackInput.zod),
-      async (c) =>
-        jsonRequest("ProviderRoutes.oauth.callback", c, function* () {
-          const providerID = c.req.valid("param").providerID
-          const { method, code } = c.req.valid("json")
-          const svc = yield* ProviderAuth.Service
-          yield* svc.callback({
-            providerID,
-            method,
-            code,
-          })
-          return true
-        }),
-    ),
-)

+ 0 - 340
packages/opencode/src/server/routes/instance/pty.ts

@@ -1,340 +0,0 @@
-import { Hono } from "hono"
-import type { Context } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import type { UpgradeWebSocket } from "hono/ws"
-import { Effect, Schema } from "effect"
-import z from "zod"
-import { AppRuntime } from "@/effect/app-runtime"
-import { Pty } from "@/pty"
-import { PtyID } from "@/pty/schema"
-import { PtyTicket } from "@/pty/ticket"
-import { Shell } from "@/shell/shell"
-import { NotFoundError } from "@/storage/storage"
-import { errors } from "../../error"
-import { jsonRequest, runRequest } from "./trace"
-import { HTTPException } from "hono/http-exception"
-import { isAllowedRequestOrigin, type CorsOptions } from "@/server/cors"
-import {
-  PTY_CONNECT_TICKET_QUERY,
-  PTY_CONNECT_TOKEN_HEADER,
-  PTY_CONNECT_TOKEN_HEADER_VALUE,
-} from "@/server/shared/pty-ticket"
-import { zod as effectZod } from "@/util/effect-zod"
-
-const ShellItem = z.object({
-  path: z.string(),
-  name: z.string(),
-  acceptable: z.boolean(),
-})
-const decodePtyID = Schema.decodeUnknownSync(PtyID)
-
-function validOrigin(c: Context, opts?: CorsOptions) {
-  return isAllowedRequestOrigin(c.req.header("origin"), c.req.header("host"), opts)
-}
-
-export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket, opts?: CorsOptions) {
-  return new Hono()
-    .get(
-      "/shells",
-      describeRoute({
-        summary: "List available shells",
-        description: "Get a list of available shells on the system.",
-        operationId: "pty.shells",
-        responses: {
-          200: {
-            description: "List of shells",
-            content: {
-              "application/json": {
-                schema: resolver(z.array(ShellItem)),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        return c.json(await Shell.list())
-      },
-    )
-    .get(
-      "/",
-      describeRoute({
-        summary: "List PTY sessions",
-        description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.",
-        operationId: "pty.list",
-        responses: {
-          200: {
-            description: "List of sessions",
-            content: {
-              "application/json": {
-                schema: resolver(Pty.Info.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("PtyRoutes.list", c, function* () {
-          const pty = yield* Pty.Service
-          return yield* pty.list()
-        }),
-    )
-    .post(
-      "/",
-      describeRoute({
-        summary: "Create PTY session",
-        description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
-        operationId: "pty.create",
-        responses: {
-          200: {
-            description: "Created session",
-            content: {
-              "application/json": {
-                schema: resolver(Pty.Info.zod),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", Pty.CreateInput.zod),
-      async (c) =>
-        jsonRequest("PtyRoutes.create", c, function* () {
-          const pty = yield* Pty.Service
-          return yield* pty.create(c.req.valid("json") as Pty.CreateInput)
-        }),
-    )
-    .get(
-      "/:ptyID",
-      describeRoute({
-        summary: "Get PTY session",
-        description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
-        operationId: "pty.get",
-        responses: {
-          200: {
-            description: "Session info",
-            content: {
-              "application/json": {
-                schema: resolver(Pty.Info.zod),
-              },
-            },
-          },
-          ...errors(404),
-        },
-      }),
-      validator("param", z.object({ ptyID: PtyID.zod })),
-      async (c) => {
-        const info = await runRequest(
-          "PtyRoutes.get",
-          c,
-          Effect.gen(function* () {
-            const pty = yield* Pty.Service
-            return yield* pty.get(c.req.valid("param").ptyID)
-          }),
-        )
-        if (!info) {
-          throw new NotFoundError({ message: "Session not found" })
-        }
-        return c.json(info)
-      },
-    )
-    .put(
-      "/:ptyID",
-      describeRoute({
-        summary: "Update PTY session",
-        description: "Update properties of an existing pseudo-terminal (PTY) session.",
-        operationId: "pty.update",
-        responses: {
-          200: {
-            description: "Updated session",
-            content: {
-              "application/json": {
-                schema: resolver(Pty.Info.zod),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("param", z.object({ ptyID: PtyID.zod })),
-      validator("json", Pty.UpdateInput.zod),
-      async (c) =>
-        jsonRequest("PtyRoutes.update", c, function* () {
-          const pty = yield* Pty.Service
-          return yield* pty.update(c.req.valid("param").ptyID, c.req.valid("json") as Pty.UpdateInput)
-        }),
-    )
-    .delete(
-      "/:ptyID",
-      describeRoute({
-        summary: "Remove PTY session",
-        description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
-        operationId: "pty.remove",
-        responses: {
-          200: {
-            description: "Session removed",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(404),
-        },
-      }),
-      validator("param", z.object({ ptyID: PtyID.zod })),
-      async (c) =>
-        jsonRequest("PtyRoutes.remove", c, function* () {
-          const pty = yield* Pty.Service
-          yield* pty.remove(c.req.valid("param").ptyID)
-          return true
-        }),
-    )
-    .post(
-      "/:ptyID/connect-token",
-      describeRoute({
-        summary: "Create PTY WebSocket token",
-        description: "Create a short-lived token for opening a PTY WebSocket connection.",
-        operationId: "pty.connectToken",
-        responses: {
-          200: {
-            description: "WebSocket connect token",
-            content: {
-              "application/json": {
-                schema: resolver(effectZod(PtyTicket.ConnectToken)),
-              },
-            },
-          },
-          ...errors(403, 404),
-        },
-      }),
-      validator("param", z.object({ ptyID: PtyID.zod })),
-      async (c) => {
-        if (c.req.header(PTY_CONNECT_TOKEN_HEADER) !== PTY_CONNECT_TOKEN_HEADER_VALUE || !validOrigin(c, opts))
-          throw new HTTPException(403)
-        const result = await runRequest(
-          "PtyRoutes.connectToken",
-          c,
-          Effect.gen(function* () {
-            const pty = yield* Pty.Service
-            const id = c.req.valid("param").ptyID
-            if (!(yield* pty.get(id))) return
-            const tickets = yield* PtyTicket.Service
-            return yield* tickets.issue({ ptyID: id, ...(yield* PtyTicket.scope) })
-          }),
-        )
-        if (!result) throw new NotFoundError({ message: "Session not found" })
-        return c.json(result)
-      },
-    )
-    .get(
-      "/:ptyID/connect",
-      describeRoute({
-        summary: "Connect to PTY session",
-        description: "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
-        operationId: "pty.connect",
-        responses: {
-          200: {
-            description: "Connected session",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(403, 404),
-        },
-      }),
-      validator("param", z.object({ ptyID: PtyID.zod })),
-      upgradeWebSocket(async (c) => {
-        type Handler = {
-          onMessage: (message: string | ArrayBuffer) => void
-          onClose: () => void
-        }
-
-        const id = decodePtyID(c.req.param("ptyID"))
-        if (
-          !(await runRequest(
-            "PtyRoutes.connect",
-            c,
-            Effect.gen(function* () {
-              const pty = yield* Pty.Service
-              return yield* pty.get(id)
-            }),
-          ))
-        ) {
-          throw new NotFoundError({ message: "Session not found" })
-        }
-        const ticket = c.req.query(PTY_CONNECT_TICKET_QUERY)
-        if (ticket) {
-          if (!validOrigin(c, opts)) throw new HTTPException(403)
-          const valid = await runRequest(
-            "PtyRoutes.connect.ticket",
-            c,
-            Effect.gen(function* () {
-              const tickets = yield* PtyTicket.Service
-              return yield* tickets.consume({ ticket, ptyID: id, ...(yield* PtyTicket.scope) })
-            }),
-          )
-          if (!valid) throw new HTTPException(403)
-        }
-        const cursor = (() => {
-          const value = c.req.query("cursor")
-          if (!value) return
-          const parsed = Number(value)
-          if (!Number.isSafeInteger(parsed) || parsed < -1) return
-          return parsed
-        })()
-        let handler: Handler | undefined
-
-        type Socket = {
-          readyState: number
-          send: (data: string | Uint8Array | ArrayBuffer) => void
-          close: (code?: number, reason?: string) => void
-        }
-
-        const isSocket = (value: unknown): value is Socket => {
-          if (!value || typeof value !== "object") return false
-          if (!("readyState" in value)) return false
-          if (!("send" in value) || typeof (value as { send?: unknown }).send !== "function") return false
-          if (!("close" in value) || typeof (value as { close?: unknown }).close !== "function") return false
-          return typeof (value as { readyState?: unknown }).readyState === "number"
-        }
-
-        const pending: string[] = []
-        let ready = false
-
-        return {
-          async onOpen(_event, ws) {
-            const socket = ws.raw
-            if (!isSocket(socket)) {
-              ws.close()
-              return
-            }
-            handler = await AppRuntime.runPromise(
-              Effect.gen(function* () {
-                const pty = yield* Pty.Service
-                return yield* pty.connect(id, socket, cursor)
-              }).pipe(Effect.withSpan("PtyRoutes.connect.open")),
-            )
-            ready = true
-            for (const msg of pending) handler?.onMessage(msg)
-            pending.length = 0
-          },
-          onMessage(event) {
-            if (typeof event.data !== "string") return
-            if (!ready) {
-              pending.push(event.data)
-              return
-            }
-            handler?.onMessage(event.data)
-          },
-          onClose() {
-            handler?.onClose()
-          },
-          onError() {
-            handler?.onClose()
-          },
-        }
-      }),
-    )
-}

+ 0 - 111
packages/opencode/src/server/routes/instance/question.ts

@@ -1,111 +0,0 @@
-import { Hono } from "hono"
-import { describeRoute, validator } from "hono-openapi"
-import { resolver } from "hono-openapi"
-import { QuestionID } from "@/question/schema"
-import { Question } from "@/question"
-import z from "zod"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { jsonRequest } from "./trace"
-
-const Reply = z.object({
-  answers: Question.Answer.zod
-    .array()
-    .describe("User answers in order of questions (each answer is an array of selected labels)"),
-})
-
-export const QuestionRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/",
-      describeRoute({
-        summary: "List pending questions",
-        description: "Get all pending question requests across all sessions.",
-        operationId: "question.list",
-        responses: {
-          200: {
-            description: "List of pending questions",
-            content: {
-              "application/json": {
-                schema: resolver(Question.Request.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) =>
-        jsonRequest("QuestionRoutes.list", c, function* () {
-          const svc = yield* Question.Service
-          return yield* svc.list()
-        }),
-    )
-    .post(
-      "/:requestID/reply",
-      describeRoute({
-        summary: "Reply to question request",
-        description: "Provide answers to a question request from the AI assistant.",
-        operationId: "question.reply",
-        responses: {
-          200: {
-            description: "Question answered successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          requestID: QuestionID.zod,
-        }),
-      ),
-      validator("json", Reply),
-      async (c) =>
-        jsonRequest("QuestionRoutes.reply", c, function* () {
-          const params = c.req.valid("param")
-          const json = c.req.valid("json")
-          const svc = yield* Question.Service
-          yield* svc.reply({
-            requestID: params.requestID,
-            answers: json.answers,
-          })
-          return true
-        }),
-    )
-    .post(
-      "/:requestID/reject",
-      describeRoute({
-        summary: "Reject question request",
-        description: "Reject a question request from the AI assistant.",
-        operationId: "question.reject",
-        responses: {
-          200: {
-            description: "Question rejected successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          requestID: QuestionID.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("QuestionRoutes.reject", c, function* () {
-          const params = c.req.valid("param")
-          const svc = yield* Question.Service
-          yield* svc.reject(params.requestID)
-          return true
-        }),
-    ),
-)

+ 0 - 1124
packages/opencode/src/server/routes/instance/session.ts

@@ -1,1124 +0,0 @@
-import { Hono } from "hono"
-import { stream } from "hono/streaming"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import { SessionID, MessageID, PartID } from "@/session/schema"
-import z from "zod"
-import { Session } from "@/session/session"
-import { MessageV2 } from "@/session/message-v2"
-import { SessionPrompt } from "@/session/prompt"
-import { SessionRunState } from "@/session/run-state"
-import { SessionCompaction } from "@/session/compaction"
-import { SessionRevert } from "@/session/revert"
-import { SessionShare } from "@/share/session"
-import { SessionStatus } from "@/session/status"
-import { SessionSummary } from "@/session/summary"
-import { Todo } from "@/session/todo"
-import { Effect } from "effect"
-import { Agent } from "@/agent/agent"
-import { Snapshot } from "@/snapshot"
-import { Command } from "@/command"
-import * as Log from "@opencode-ai/core/util/log"
-import { Permission } from "@/permission"
-import { PermissionID } from "@/permission/schema"
-import { ModelID, ProviderID } from "@/provider/schema"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { zodObject } from "@/util/effect-zod"
-import { Bus } from "@/bus"
-import { NamedError } from "@opencode-ai/core/util/error"
-import { jsonRequest, runRequest } from "./trace"
-
-const log = Log.create({ service: "server" })
-
-const QueryBoolean = z.union([
-  z.preprocess((value) => (value === "true" ? true : value === "false" ? false : value), z.boolean()),
-  z.enum(["true", "false"]),
-])
-
-function queryBoolean(value: z.infer<typeof QueryBoolean> | undefined) {
-  if (value === undefined) return
-  return value === true || value === "true"
-}
-
-export const SessionRoutes = lazy(() =>
-  new Hono()
-    .get(
-      "/",
-      describeRoute({
-        summary: "List sessions",
-        description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
-        operationId: "session.list",
-        responses: {
-          200: {
-            description: "List of sessions",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "query",
-        z.object({
-          directory: z.string().optional().meta({ description: "Filter sessions by directory" }),
-          // TODO: in 2.0 remove `scope` and `directory` and default
-          // to list all sessions for a project
-          scope: z.enum(["project"]).optional().meta({ description: "List all sessions for the current project" }),
-          path: z.string().optional().meta({ description: "Filter sessions by project-relative path" }),
-          roots: QueryBoolean.optional().meta({ description: "Only return root sessions (no parentID)" }),
-          start: z.coerce
-            .number()
-            .optional()
-            .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
-          search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
-          limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
-        }),
-      ),
-      async (c) => {
-        const query = c.req.valid("query")
-        return c.json(
-          await runRequest(
-            "SessionRoutes.list",
-            c,
-            Session.Service.use((svc) =>
-              svc.list({
-                directory: query.scope === "project" ? undefined : query.directory,
-                path: query.path,
-                roots: queryBoolean(query.roots),
-                start: query.start,
-                search: query.search,
-                limit: query.limit,
-              }),
-            ),
-          ),
-        )
-      },
-    )
-    .get(
-      "/status",
-      describeRoute({
-        summary: "Get session status",
-        description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
-        operationId: "session.status",
-        responses: {
-          200: {
-            description: "Get session status",
-            content: {
-              "application/json": {
-                schema: resolver(z.record(z.string(), SessionStatus.Info.zod)),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      async (c) =>
-        jsonRequest("SessionRoutes.status", c, function* () {
-          const svc = yield* SessionStatus.Service
-          return Object.fromEntries(yield* svc.list())
-        }),
-    )
-    .get(
-      "/:sessionID",
-      describeRoute({
-        summary: "Get session",
-        description: "Retrieve detailed information about a specific OpenCode session.",
-        tags: ["Session"],
-        operationId: "session.get",
-        responses: {
-          200: {
-            description: "Get session",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: Session.GetInput.zod,
-        }),
-      ),
-      async (c) => {
-        const sessionID = c.req.valid("param").sessionID
-        return jsonRequest("SessionRoutes.get", c, function* () {
-          const session = yield* Session.Service
-          return yield* session.get(sessionID)
-        })
-      },
-    )
-    .get(
-      "/:sessionID/children",
-      describeRoute({
-        summary: "Get session children",
-        tags: ["Session"],
-        description: "Retrieve all child sessions that were forked from the specified parent session.",
-        operationId: "session.children",
-        responses: {
-          200: {
-            description: "List of children",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod.array()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: Session.ChildrenInput.zod,
-        }),
-      ),
-      async (c) => {
-        const sessionID = c.req.valid("param").sessionID
-        return jsonRequest("SessionRoutes.children", c, function* () {
-          const session = yield* Session.Service
-          return yield* session.children(sessionID)
-        })
-      },
-    )
-    .get(
-      "/:sessionID/todo",
-      describeRoute({
-        summary: "Get session todos",
-        description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
-        operationId: "session.todo",
-        responses: {
-          200: {
-            description: "Todo list",
-            content: {
-              "application/json": {
-                schema: resolver(Todo.Info.zod.array()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      async (c) => {
-        const sessionID = c.req.valid("param").sessionID
-        return jsonRequest("SessionRoutes.todo", c, function* () {
-          const todo = yield* Todo.Service
-          return yield* todo.get(sessionID)
-        })
-      },
-    )
-    .post(
-      "/",
-      describeRoute({
-        summary: "Create session",
-        description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
-        operationId: "session.create",
-        responses: {
-          ...errors(400),
-          200: {
-            description: "Successfully created session",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod),
-              },
-            },
-          },
-        },
-      }),
-      validator("json", Session.CreateInput.zod),
-      async (c) =>
-        jsonRequest("SessionRoutes.create", c, function* () {
-          const body = c.req.valid("json") ?? {}
-          const svc = yield* SessionShare.Service
-          return yield* svc.create(body)
-        }),
-    )
-    .delete(
-      "/:sessionID",
-      describeRoute({
-        summary: "Delete session",
-        description: "Delete a session and permanently remove all associated data, including messages and history.",
-        operationId: "session.delete",
-        responses: {
-          200: {
-            description: "Successfully deleted session",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: Session.RemoveInput.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.delete", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const svc = yield* Session.Service
-          yield* svc.remove(sessionID)
-          return true
-        }),
-    )
-    .patch(
-      "/:sessionID",
-      describeRoute({
-        summary: "Update session",
-        description: "Update properties of an existing session, such as title or other metadata.",
-        operationId: "session.update",
-        responses: {
-          200: {
-            description: "Successfully updated session",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator(
-        "json",
-        z.object({
-          title: z.string().optional(),
-          permission: Permission.Ruleset.zod.optional(),
-          time: z
-            .object({
-              archived: z.number().optional(),
-            })
-            .optional(),
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.update", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const updates = c.req.valid("json")
-          const session = yield* Session.Service
-          const current = yield* session.get(sessionID)
-
-          if (updates.title !== undefined) {
-            yield* session.setTitle({ sessionID, title: updates.title })
-          }
-          if (updates.permission !== undefined) {
-            yield* session.setPermission({
-              sessionID,
-              permission: Permission.merge(current.permission ?? [], updates.permission),
-            })
-          }
-          if (updates.time?.archived !== undefined) {
-            yield* session.setArchived({ sessionID, time: updates.time.archived })
-          }
-
-          return yield* session.get(sessionID)
-        }),
-    )
-    // TODO(v2): remove this dedicated route and rely on the normal `/init` command flow.
-    .post(
-      "/:sessionID/init",
-      describeRoute({
-        summary: "Initialize session",
-        description:
-          "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
-        operationId: "session.init",
-        responses: {
-          200: {
-            description: "200",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator(
-        "json",
-        z.object({
-          modelID: ModelID.zod,
-          providerID: ProviderID.zod,
-          messageID: MessageID.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.init", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const body = c.req.valid("json")
-          const svc = yield* SessionPrompt.Service
-          yield* svc.command({
-            sessionID,
-            messageID: body.messageID,
-            model: body.providerID + "/" + body.modelID,
-            command: Command.Default.INIT,
-            arguments: "",
-          })
-          return true
-        }),
-    )
-    .post(
-      "/:sessionID/fork",
-      describeRoute({
-        summary: "Fork session",
-        description: "Create a new session by forking an existing session at a specific message point.",
-        operationId: "session.fork",
-        responses: {
-          200: {
-            description: "200",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator("json", zodObject(Session.ForkInput).omit({ sessionID: true })),
-      async (c) =>
-        jsonRequest("SessionRoutes.fork", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const body = c.req.valid("json") as { messageID?: MessageID }
-          const svc = yield* Session.Service
-          return yield* svc.fork({ ...body, sessionID })
-        }),
-    )
-    .post(
-      "/:sessionID/abort",
-      describeRoute({
-        summary: "Abort session",
-        description: "Abort an active session and stop any ongoing AI processing or command execution.",
-        operationId: "session.abort",
-        responses: {
-          200: {
-            description: "Aborted session",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.abort", c, function* () {
-          const svc = yield* SessionPrompt.Service
-          yield* svc.cancel(c.req.valid("param").sessionID)
-          return true
-        }),
-    )
-    .post(
-      "/:sessionID/share",
-      describeRoute({
-        summary: "Share session",
-        description: "Create a shareable link for a session, allowing others to view the conversation.",
-        operationId: "session.share",
-        responses: {
-          200: {
-            description: "Successfully shared session",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.share", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const share = yield* SessionShare.Service
-          const session = yield* Session.Service
-          yield* share.share(sessionID)
-          return yield* session.get(sessionID)
-        }),
-    )
-    .get(
-      "/:sessionID/diff",
-      describeRoute({
-        summary: "Get message diff",
-        description: "Get the file changes (diff) that resulted from a specific user message in the session.",
-        operationId: "session.diff",
-        responses: {
-          200: {
-            description: "Successfully retrieved diff",
-            content: {
-              "application/json": {
-                schema: resolver(Snapshot.FileDiff.zod.array()),
-              },
-            },
-          },
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator("query", zodObject(SessionSummary.DiffInput).omit({ sessionID: true })),
-      async (c) =>
-        jsonRequest("SessionRoutes.diff", c, function* () {
-          const query = c.req.valid("query") as Omit<SessionSummary.DiffInput, "sessionID">
-          const params = c.req.valid("param")
-          const summary = yield* SessionSummary.Service
-          return yield* summary.diff({
-            sessionID: params.sessionID,
-            messageID: query.messageID,
-          })
-        }),
-    )
-    .delete(
-      "/:sessionID/share",
-      describeRoute({
-        summary: "Unshare session",
-        description: "Remove the shareable link for a session, making it private again.",
-        operationId: "session.unshare",
-        responses: {
-          200: {
-            description: "Successfully unshared session",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.unshare", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const share = yield* SessionShare.Service
-          const session = yield* Session.Service
-          yield* share.unshare(sessionID)
-          return yield* session.get(sessionID)
-        }),
-    )
-    .post(
-      "/:sessionID/summarize",
-      describeRoute({
-        summary: "Summarize session",
-        description: "Generate a concise summary of the session using AI compaction to preserve key information.",
-        operationId: "session.summarize",
-        responses: {
-          200: {
-            description: "Summarized session",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator(
-        "json",
-        z.object({
-          providerID: ProviderID.zod,
-          modelID: ModelID.zod,
-          auto: z.boolean().optional().default(false),
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.summarize", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const body = c.req.valid("json")
-          const session = yield* Session.Service
-          const revert = yield* SessionRevert.Service
-          const compact = yield* SessionCompaction.Service
-          const prompt = yield* SessionPrompt.Service
-          const agent = yield* Agent.Service
-
-          yield* revert.cleanup(yield* session.get(sessionID))
-          const msgs = yield* session.messages({ sessionID })
-          const defaultAgent = yield* agent.defaultAgent()
-          let currentAgent = defaultAgent
-          for (let i = msgs.length - 1; i >= 0; i--) {
-            const info = msgs[i].info
-            if (info.role === "user") {
-              currentAgent = info.agent || defaultAgent
-              break
-            }
-          }
-
-          yield* compact.create({
-            sessionID,
-            agent: currentAgent,
-            model: {
-              providerID: body.providerID,
-              modelID: body.modelID,
-            },
-            auto: body.auto,
-          })
-          yield* prompt.loop({ sessionID })
-          return true
-        }),
-    )
-    .get(
-      "/:sessionID/message",
-      describeRoute({
-        summary: "Get session messages",
-        description: "Retrieve all messages in a session, including user prompts and AI responses.",
-        operationId: "session.messages",
-        responses: {
-          200: {
-            description: "List of messages",
-            content: {
-              "application/json": {
-                schema: resolver(MessageV2.WithParts.zod.array()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator(
-        "query",
-        z
-          .object({
-            limit: z.coerce
-              .number()
-              .int()
-              .min(0)
-              .optional()
-              .meta({ description: "Maximum number of messages to return" }),
-            before: z
-              .string()
-              .optional()
-              .meta({ description: "Opaque cursor for loading older messages" })
-              .refine(
-                (value) => {
-                  if (!value) return true
-                  try {
-                    MessageV2.cursor.decode(value)
-                    return true
-                  } catch {
-                    return false
-                  }
-                },
-                { message: "Invalid cursor" },
-              ),
-          })
-          .refine((value) => !value.before || value.limit !== undefined, {
-            message: "before requires limit",
-            path: ["before"],
-          }),
-      ),
-      async (c) => {
-        const query = c.req.valid("query")
-        const sessionID = c.req.valid("param").sessionID
-        if (query.limit === undefined || query.limit === 0) {
-          const messages = await runRequest(
-            "SessionRoutes.messages",
-            c,
-            Effect.gen(function* () {
-              const session = yield* Session.Service
-              yield* session.get(sessionID)
-              return yield* session.messages({ sessionID })
-            }),
-          )
-          return c.json(messages)
-        }
-
-        const page = await MessageV2.page({
-          sessionID,
-          limit: query.limit,
-          before: query.before,
-        })
-        if (page.cursor) {
-          const url = new URL(c.req.url)
-          url.searchParams.set("limit", query.limit.toString())
-          url.searchParams.set("before", page.cursor)
-          c.header("Access-Control-Expose-Headers", "Link, X-Next-Cursor")
-          c.header("Link", `<${url.toString()}>; rel="next"`)
-          c.header("X-Next-Cursor", page.cursor)
-        }
-        return c.json(page.items)
-      },
-    )
-    .get(
-      "/:sessionID/message/:messageID",
-      describeRoute({
-        summary: "Get message",
-        description: "Retrieve a specific message from a session by its message ID.",
-        operationId: "session.message",
-        responses: {
-          200: {
-            description: "Message",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z.object({
-                    info: MessageV2.Info.zod,
-                    parts: MessageV2.Part.zod.array(),
-                  }),
-                ),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-          messageID: MessageID.zod,
-        }),
-      ),
-      async (c) => {
-        const params = c.req.valid("param")
-        const message = await MessageV2.get({
-          sessionID: params.sessionID,
-          messageID: params.messageID,
-        })
-        return c.json(message)
-      },
-    )
-    .delete(
-      "/:sessionID/message/:messageID",
-      describeRoute({
-        summary: "Delete message",
-        description:
-          "Permanently delete a specific message (and all of its parts) from a session. This does not revert any file changes that may have been made while processing the message.",
-        operationId: "session.deleteMessage",
-        responses: {
-          200: {
-            description: "Successfully deleted message",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-          messageID: MessageID.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.deleteMessage", c, function* () {
-          const params = c.req.valid("param")
-          const state = yield* SessionRunState.Service
-          const session = yield* Session.Service
-          yield* state.assertNotBusy(params.sessionID)
-          yield* session.removeMessage({
-            sessionID: params.sessionID,
-            messageID: params.messageID,
-          })
-          return true
-        }),
-    )
-    .delete(
-      "/:sessionID/message/:messageID/part/:partID",
-      describeRoute({
-        description: "Delete a part from a message",
-        operationId: "part.delete",
-        responses: {
-          200: {
-            description: "Successfully deleted part",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-          messageID: MessageID.zod,
-          partID: PartID.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.deletePart", c, function* () {
-          const params = c.req.valid("param")
-          const svc = yield* Session.Service
-          yield* svc.removePart({
-            sessionID: params.sessionID,
-            messageID: params.messageID,
-            partID: params.partID,
-          })
-          return true
-        }),
-    )
-    .patch(
-      "/:sessionID/message/:messageID/part/:partID",
-      describeRoute({
-        description: "Update a part in a message",
-        operationId: "part.update",
-        responses: {
-          200: {
-            description: "Successfully updated part",
-            content: {
-              "application/json": {
-                schema: resolver(MessageV2.Part.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-          messageID: MessageID.zod,
-          partID: PartID.zod,
-        }),
-      ),
-      validator("json", MessageV2.Part.zod),
-      async (c) => {
-        const params = c.req.valid("param")
-        const body = c.req.valid("json")
-        if (body.id !== params.partID || body.messageID !== params.messageID || body.sessionID !== params.sessionID) {
-          throw new Error(
-            `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`,
-          )
-        }
-        return jsonRequest("SessionRoutes.updatePart", c, function* () {
-          const svc = yield* Session.Service
-          return yield* svc.updatePart(body)
-        })
-      },
-    )
-    .post(
-      "/:sessionID/message",
-      describeRoute({
-        summary: "Send message",
-        description: "Create and send a new message to a session, streaming the AI response.",
-        operationId: "session.prompt",
-        responses: {
-          200: {
-            description: "Created message",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z.object({
-                    info: MessageV2.Assistant.zod,
-                    parts: MessageV2.Part.zod.array(),
-                  }),
-                ),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator("json", zodObject(SessionPrompt.PromptInput).omit({ sessionID: true })),
-      async (c) => {
-        c.status(200)
-        c.header("Content-Type", "application/json")
-        return stream(c, async (stream) => {
-          const sessionID = c.req.valid("param").sessionID
-          const body = c.req.valid("json")
-          const msg = await runRequest(
-            "SessionRoutes.prompt",
-            c,
-            SessionPrompt.Service.use((svc) =>
-              svc.prompt({ ...body, sessionID } as unknown as SessionPrompt.PromptInput),
-            ),
-          )
-          void stream.write(JSON.stringify(msg))
-        })
-      },
-    )
-    .post(
-      "/:sessionID/prompt_async",
-      describeRoute({
-        summary: "Send async message",
-        description:
-          "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
-        operationId: "session.prompt_async",
-        responses: {
-          204: {
-            description: "Prompt accepted",
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator("json", zodObject(SessionPrompt.PromptInput).omit({ sessionID: true })),
-      async (c) => {
-        const sessionID = c.req.valid("param").sessionID
-        const body = c.req.valid("json")
-        void runRequest(
-          "SessionRoutes.prompt_async",
-          c,
-          SessionPrompt.Service.use((svc) =>
-            svc.prompt({ ...body, sessionID } as unknown as SessionPrompt.PromptInput),
-          ),
-        ).catch((err) => {
-          log.error("prompt_async failed", { sessionID, error: err })
-          void Bus.publish(Session.Event.Error, {
-            sessionID,
-            error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(),
-          })
-        })
-
-        return c.body(null, 204)
-      },
-    )
-    .post(
-      "/:sessionID/command",
-      describeRoute({
-        summary: "Send command",
-        description: "Send a new command to a session for execution by the AI assistant.",
-        operationId: "session.command",
-        responses: {
-          200: {
-            description: "Created message",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z.object({
-                    info: MessageV2.Assistant.zod,
-                    parts: MessageV2.Part.zod.array(),
-                  }),
-                ),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator("json", zodObject(SessionPrompt.CommandInput).omit({ sessionID: true })),
-      async (c) =>
-        jsonRequest("SessionRoutes.command", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const body = c.req.valid("json") as Omit<SessionPrompt.CommandInput, "sessionID">
-          const svc = yield* SessionPrompt.Service
-          return yield* svc.command({ ...body, sessionID })
-        }),
-    )
-    .post(
-      "/:sessionID/shell",
-      describeRoute({
-        summary: "Run shell command",
-        description: "Execute a shell command within the session context and return the AI's response.",
-        operationId: "session.shell",
-        responses: {
-          200: {
-            description: "Created message",
-            content: {
-              "application/json": {
-                schema: resolver(MessageV2.WithParts.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator("json", zodObject(SessionPrompt.ShellInput).omit({ sessionID: true })),
-      async (c) =>
-        jsonRequest("SessionRoutes.shell", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const body = c.req.valid("json") as Omit<SessionPrompt.ShellInput, "sessionID">
-          const svc = yield* SessionPrompt.Service
-          return yield* svc.shell({ ...body, sessionID })
-        }),
-    )
-    .post(
-      "/:sessionID/revert",
-      describeRoute({
-        summary: "Revert message",
-        description: "Revert a specific message in a session, undoing its effects and restoring the previous state.",
-        operationId: "session.revert",
-        responses: {
-          200: {
-            description: "Updated session",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      validator("json", zodObject(SessionRevert.RevertInput).omit({ sessionID: true })),
-      async (c) => {
-        const sessionID = c.req.valid("param").sessionID
-        const body = c.req.valid("json") as Omit<SessionRevert.RevertInput, "sessionID">
-        log.info("revert", body)
-        return jsonRequest("SessionRoutes.revert", c, function* () {
-          const svc = yield* SessionRevert.Service
-          return yield* svc.revert({ sessionID, ...body })
-        })
-      },
-    )
-    .post(
-      "/:sessionID/unrevert",
-      describeRoute({
-        summary: "Restore reverted messages",
-        description: "Restore all previously reverted messages in a session.",
-        operationId: "session.unrevert",
-        responses: {
-          200: {
-            description: "Updated session",
-            content: {
-              "application/json": {
-                schema: resolver(Session.Info.zod),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-        }),
-      ),
-      async (c) =>
-        jsonRequest("SessionRoutes.unrevert", c, function* () {
-          const sessionID = c.req.valid("param").sessionID
-          const svc = yield* SessionRevert.Service
-          return yield* svc.unrevert({ sessionID })
-        }),
-    )
-    .post(
-      "/:sessionID/permissions/:permissionID",
-      describeRoute({
-        summary: "Respond to permission",
-        deprecated: true,
-        description: "Approve or deny a permission request from the AI assistant.",
-        operationId: "permission.respond",
-        responses: {
-          200: {
-            description: "Permission processed successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator(
-        "param",
-        z.object({
-          sessionID: SessionID.zod,
-          permissionID: PermissionID.zod,
-        }),
-      ),
-      validator("json", z.object({ response: Permission.Reply.zod })),
-      async (c) =>
-        jsonRequest("SessionRoutes.permissionRespond", c, function* () {
-          const params = c.req.valid("param")
-          const svc = yield* Permission.Service
-          yield* svc.reply({
-            requestID: params.permissionID,
-            reply: c.req.valid("json").response,
-          })
-          return true
-        }),
-    ),
-)

+ 0 - 199
packages/opencode/src/server/routes/instance/sync.ts

@@ -1,199 +0,0 @@
-import z from "zod"
-import { Hono } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import { SyncEvent } from "@/sync"
-import { Database } from "@/storage/db"
-import { asc } from "drizzle-orm"
-import { and } from "drizzle-orm"
-import { not } from "drizzle-orm"
-import { or } from "drizzle-orm"
-import { lte } from "drizzle-orm"
-import { eq } from "drizzle-orm"
-import { EventTable } from "@/sync/event.sql"
-import { lazy } from "@/util/lazy"
-import * as Log from "@opencode-ai/core/util/log"
-import { Workspace } from "@/control-plane/workspace"
-import { AppRuntime } from "@/effect/app-runtime"
-import { Instance } from "@/project/instance"
-import { errors } from "../../error"
-import { Session } from "@/session/session"
-import { WorkspaceContext } from "@/control-plane/workspace-context"
-import { SessionID } from "@/session/schema"
-
-const ReplayEvent = z.object({
-  id: z.string(),
-  aggregateID: z.string(),
-  seq: z.number().int().min(0),
-  type: z.string(),
-  data: z.record(z.string(), z.unknown()),
-})
-const SessionPayload = z.object({
-  sessionID: SessionID.zod,
-})
-
-const log = Log.create({ service: "server.sync" })
-
-export const SyncRoutes = lazy(() =>
-  new Hono()
-    .post(
-      "/start",
-      describeRoute({
-        summary: "Start workspace sync",
-        description: "Start sync loops for workspaces in the current project that have active sessions.",
-        operationId: "sync.start",
-        responses: {
-          200: {
-            description: "Workspace sync started",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        void AppRuntime.runPromise(
-          Workspace.Service.use((workspace) => workspace.startWorkspaceSyncing(Instance.project.id)),
-        )
-        return c.json(true)
-      },
-    )
-    .post(
-      "/replay",
-      describeRoute({
-        summary: "Replay sync events",
-        description: "Validate and replay a complete sync event history.",
-        operationId: "sync.replay",
-        responses: {
-          200: {
-            description: "Replayed sync events",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z.object({
-                    sessionID: z.string(),
-                  }),
-                ),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "json",
-        z.object({
-          directory: z.string(),
-          events: z.array(ReplayEvent).min(1),
-        }),
-      ),
-      async (c) => {
-        const body = c.req.valid("json")
-        const events = body.events
-        const source = events[0].aggregateID
-
-        log.info("sync replay requested", {
-          sessionID: source,
-          events: events.length,
-          first: events[0]?.seq,
-          last: events.at(-1)?.seq,
-          directory: body.directory,
-        })
-        await AppRuntime.runPromise(SyncEvent.use.replayAll(events))
-
-        log.info("sync replay complete", {
-          sessionID: source,
-          events: events.length,
-          first: events[0]?.seq,
-          last: events.at(-1)?.seq,
-        })
-
-        return c.json({
-          sessionID: source,
-        })
-      },
-    )
-    .post(
-      "/steal",
-      describeRoute({
-        summary: "Steal session into workspace",
-        description: "Update a session to belong to the current workspace through the sync event system.",
-        operationId: "sync.steal",
-        responses: {
-          200: {
-            description: "Session stolen into workspace",
-            content: {
-              "application/json": {
-                schema: resolver(SessionPayload),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", SessionPayload),
-      async (c) => {
-        const body = c.req.valid("json")
-        const workspaceID = WorkspaceContext.workspaceID
-        if (!workspaceID) throw new Error("Cannot steal session without workspace context")
-
-        SyncEvent.run(Session.Event.Updated, {
-          sessionID: body.sessionID,
-          info: {
-            workspaceID,
-          },
-        })
-
-        log.info("sync session stolen", {
-          sessionID: body.sessionID,
-          workspaceID,
-        })
-
-        return c.json({
-          sessionID: body.sessionID,
-        })
-      },
-    )
-    .post(
-      "/history",
-      describeRoute({
-        summary: "List sync events",
-        description:
-          "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.",
-        operationId: "sync.history.list",
-        responses: {
-          200: {
-            description: "Sync events",
-            content: {
-              "application/json": {
-                schema: resolver(
-                  z.array(
-                    z.object({
-                      id: z.string(),
-                      aggregate_id: z.string(),
-                      seq: z.number(),
-                      type: z.string(),
-                      data: z.record(z.string(), z.unknown()),
-                    }),
-                  ),
-                ),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", z.record(z.string(), z.number().int().min(0))),
-      async (c) => {
-        const body = c.req.valid("json")
-        const exclude = Object.entries(body)
-        const where =
-          exclude.length > 0
-            ? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!)
-            : undefined
-        const rows = Database.use((db) => db.select().from(EventTable).where(where).orderBy(asc(EventTable.seq)).all())
-        return c.json(rows)
-      },
-    ),
-)

+ 0 - 59
packages/opencode/src/server/routes/instance/trace.ts

@@ -1,59 +0,0 @@
-import type { Context } from "hono"
-import { Effect } from "effect"
-import { AppRuntime } from "@/effect/app-runtime"
-
-type AppEnv = Parameters<typeof AppRuntime.runPromise>[0] extends Effect.Effect<any, any, infer R> ? R : never
-
-// Build the base span attributes for an HTTP handler: method, path, and every
-// matched route param. Names follow OTel attribute-naming guidance:
-// domain-first (`session.id`, `message.id`, …) so they match the existing
-// OTel `session.id` semantic convention and the bare `message.id` we
-// already emit from Tool.execute. Non-standard route params fall back to
-// `opencode.<name>` since those are internal implementation details
-// (per https://opentelemetry.io/blog/2025/how-to-name-your-span-attributes/).
-export interface RequestLike {
-  readonly req: {
-    readonly method: string
-    readonly url: string
-    param(): Record<string, string>
-  }
-}
-
-// Normalize a Hono route param key (e.g. `sessionID`, `messageID`, `name`)
-// to an OTel attribute key. `fooID` → `foo.id` for ID-shaped params; any
-// other param is namespaced under `opencode.` to avoid colliding with
-// standard conventions.
-export function paramToAttributeKey(key: string): string {
-  const m = key.match(/^(.+)ID$/)
-  if (m) return `${m[1].toLowerCase()}.id`
-  return `opencode.${key}`
-}
-
-export function requestAttributes(c: RequestLike): Record<string, string> {
-  const attributes: Record<string, string> = {
-    "http.method": c.req.method,
-    "http.path": new URL(c.req.url).pathname,
-  }
-  for (const [key, value] of Object.entries(c.req.param())) {
-    attributes[paramToAttributeKey(key)] = value
-  }
-  return attributes
-}
-
-export function runRequest<A, E>(name: string, c: Context, effect: Effect.Effect<A, E, AppEnv>) {
-  return AppRuntime.runPromise(effect.pipe(Effect.withSpan(name, { attributes: requestAttributes(c) })))
-}
-
-export async function jsonRequest<C extends Context, A, E>(
-  name: string,
-  c: C,
-  effect: (c: C) => Effect.gen.Return<A, E, AppEnv>,
-) {
-  return c.json(
-    await runRequest(
-      name,
-      c,
-      Effect.gen(() => effect(c)),
-    ),
-  )
-}

+ 0 - 387
packages/opencode/src/server/routes/instance/tui.ts

@@ -1,387 +0,0 @@
-import { Hono, type Context } from "hono"
-import { describeRoute, validator, resolver } from "hono-openapi"
-import { Schema } from "effect"
-import z from "zod"
-import { Bus } from "@/bus"
-import { Session } from "@/session/session"
-import type { SessionID } from "@/session/schema"
-import { TuiEvent } from "@/cli/cmd/tui/event"
-import { zodObject } from "@/util/effect-zod"
-import { errors } from "../../error"
-import { lazy } from "@/util/lazy"
-import { runRequest } from "./trace"
-import {
-  TuiRequest,
-  nextTuiRequest,
-  nextTuiResponse,
-  submitTuiRequest,
-  submitTuiResponse,
-} from "@/server/shared/tui-control"
-
-export async function callTui(ctx: Context) {
-  const body = await ctx.req.json()
-  submitTuiRequest({
-    path: ctx.req.path,
-    body,
-  })
-  return nextTuiResponse()
-}
-
-const TuiControlRoutes = new Hono()
-  .get(
-    "/next",
-    describeRoute({
-      summary: "Get next TUI request",
-      description: "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.",
-      operationId: "tui.control.next",
-      responses: {
-        200: {
-          description: "Next TUI request",
-          content: {
-            "application/json": {
-              schema: resolver(TuiRequest),
-            },
-          },
-        },
-      },
-    }),
-    async (c) => {
-      const req = await nextTuiRequest()
-      return c.json(req)
-    },
-  )
-  .post(
-    "/response",
-    describeRoute({
-      summary: "Submit TUI response",
-      description: "Submit a response to the TUI request queue to complete a pending request.",
-      operationId: "tui.control.response",
-      responses: {
-        200: {
-          description: "Response submitted successfully",
-          content: {
-            "application/json": {
-              schema: resolver(z.boolean()),
-            },
-          },
-        },
-      },
-    }),
-    validator("json", z.any()),
-    async (c) => {
-      const body = c.req.valid("json")
-      submitTuiResponse(body)
-      return c.json(true)
-    },
-  )
-
-export const TuiRoutes = lazy(() =>
-  new Hono()
-    .post(
-      "/append-prompt",
-      describeRoute({
-        summary: "Append TUI prompt",
-        description: "Append prompt to the TUI",
-        operationId: "tui.appendPrompt",
-        responses: {
-          200: {
-            description: "Prompt processed successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", zodObject(TuiEvent.PromptAppend.properties)),
-      async (c) => {
-        await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json") as { text: string })
-        return c.json(true)
-      },
-    )
-    .post(
-      "/open-help",
-      describeRoute({
-        summary: "Open help dialog",
-        description: "Open the help dialog in the TUI to display user assistance information.",
-        operationId: "tui.openHelp",
-        responses: {
-          200: {
-            description: "Help dialog opened successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        await Bus.publish(TuiEvent.CommandExecute, {
-          command: "help.show",
-        })
-        return c.json(true)
-      },
-    )
-    .post(
-      "/open-sessions",
-      describeRoute({
-        summary: "Open sessions dialog",
-        description: "Open the session dialog",
-        operationId: "tui.openSessions",
-        responses: {
-          200: {
-            description: "Session dialog opened successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        await Bus.publish(TuiEvent.CommandExecute, {
-          command: "session.list",
-        })
-        return c.json(true)
-      },
-    )
-    .post(
-      "/open-themes",
-      describeRoute({
-        summary: "Open themes dialog",
-        description: "Open the theme dialog",
-        operationId: "tui.openThemes",
-        responses: {
-          200: {
-            description: "Theme dialog opened successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        await Bus.publish(TuiEvent.CommandExecute, {
-          command: "session.list",
-        })
-        return c.json(true)
-      },
-    )
-    .post(
-      "/open-models",
-      describeRoute({
-        summary: "Open models dialog",
-        description: "Open the model dialog",
-        operationId: "tui.openModels",
-        responses: {
-          200: {
-            description: "Model dialog opened successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        await Bus.publish(TuiEvent.CommandExecute, {
-          command: "model.list",
-        })
-        return c.json(true)
-      },
-    )
-    .post(
-      "/submit-prompt",
-      describeRoute({
-        summary: "Submit TUI prompt",
-        description: "Submit the prompt",
-        operationId: "tui.submitPrompt",
-        responses: {
-          200: {
-            description: "Prompt submitted successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        await Bus.publish(TuiEvent.CommandExecute, {
-          command: "prompt.submit",
-        })
-        return c.json(true)
-      },
-    )
-    .post(
-      "/clear-prompt",
-      describeRoute({
-        summary: "Clear TUI prompt",
-        description: "Clear the prompt",
-        operationId: "tui.clearPrompt",
-        responses: {
-          200: {
-            description: "Prompt cleared successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      async (c) => {
-        await Bus.publish(TuiEvent.CommandExecute, {
-          command: "prompt.clear",
-        })
-        return c.json(true)
-      },
-    )
-    .post(
-      "/execute-command",
-      describeRoute({
-        summary: "Execute TUI command",
-        description: "Execute a TUI command (e.g. agent_cycle)",
-        operationId: "tui.executeCommand",
-        responses: {
-          200: {
-            description: "Command executed successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator("json", z.object({ command: z.string() })),
-      async (c) => {
-        const command = c.req.valid("json").command
-        await Bus.publish(TuiEvent.CommandExecute, {
-          // @ts-expect-error
-          command: {
-            session_new: "session.new",
-            session_share: "session.share",
-            session_interrupt: "session.interrupt",
-            session_compact: "session.compact",
-            messages_page_up: "session.page.up",
-            messages_page_down: "session.page.down",
-            messages_line_up: "session.line.up",
-            messages_line_down: "session.line.down",
-            messages_half_page_up: "session.half.page.up",
-            messages_half_page_down: "session.half.page.down",
-            messages_first: "session.first",
-            messages_last: "session.last",
-            agent_cycle: "agent.cycle",
-          }[command],
-        })
-        return c.json(true)
-      },
-    )
-    .post(
-      "/show-toast",
-      describeRoute({
-        summary: "Show TUI toast",
-        description: "Show a toast notification in the TUI",
-        operationId: "tui.showToast",
-        responses: {
-          200: {
-            description: "Toast notification shown successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-        },
-      }),
-      validator("json", zodObject(TuiEvent.ToastShow.properties)),
-      async (c) => {
-        await Bus.publish(
-          TuiEvent.ToastShow,
-          c.req.valid("json") as Schema.Schema.Type<typeof TuiEvent.ToastShow.properties>,
-        )
-        return c.json(true)
-      },
-    )
-    .post(
-      "/publish",
-      describeRoute({
-        summary: "Publish TUI event",
-        description: "Publish a TUI event",
-        operationId: "tui.publish",
-        responses: {
-          200: {
-            description: "Event published successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400),
-        },
-      }),
-      validator(
-        "json",
-        z.union(
-          Object.values(TuiEvent).map((def) => {
-            return z
-              .object({
-                type: z.literal(def.type),
-                properties: zodObject(def.properties),
-              })
-              .meta({
-                ref: `Event.${def.type}`,
-              })
-          }),
-        ),
-      ),
-      async (c) => {
-        const evt = c.req.valid("json") as { type: string; properties: Record<string, unknown> }
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        await Bus.publish(Object.values(TuiEvent).find((def) => def.type === evt.type)! as any, evt.properties as any)
-        return c.json(true)
-      },
-    )
-    .post(
-      "/select-session",
-      describeRoute({
-        summary: "Select session",
-        description: "Navigate the TUI to display the specified session.",
-        operationId: "tui.selectSession",
-        responses: {
-          200: {
-            description: "Session selected successfully",
-            content: {
-              "application/json": {
-                schema: resolver(z.boolean()),
-              },
-            },
-          },
-          ...errors(400, 404),
-        },
-      }),
-      validator("json", zodObject(TuiEvent.SessionSelect.properties)),
-      async (c) => {
-        const { sessionID } = c.req.valid("json") as { sessionID: SessionID }
-        await runRequest(
-          "TuiRoutes.sessionSelect",
-          c,
-          Session.Service.use((svc) => svc.get(sessionID)),
-        )
-        await Bus.publish(TuiEvent.SessionSelect, { sessionID })
-        return c.json(true)
-      },
-    )
-    .route("/control", TuiControlRoutes),
-)

+ 0 - 40
packages/opencode/src/server/routes/ui.ts

@@ -1,40 +0,0 @@
-import fs from "node:fs/promises"
-import { AppFileSystem } from "@opencode-ai/core/filesystem"
-import { Hono } from "hono"
-import { proxy } from "hono/proxy"
-import { ProxyUtil } from "../proxy-util"
-import { UI_UPSTREAM, csp, cspForHtml, embeddedUI, upstreamURL } from "../shared/ui"
-
-export async function serveUI(request: Request) {
-  const embeddedWebUI = await embeddedUI()
-  const path = new URL(request.url).pathname
-
-  if (embeddedWebUI) {
-    const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
-    if (!match) return Response.json({ error: "Not Found" }, { status: 404 })
-
-    if (await fs.exists(match)) {
-      const mime = AppFileSystem.mimeType(match)
-      const headers = new Headers({ "content-type": mime })
-      const body = new Uint8Array(await fs.readFile(match))
-      if (mime.startsWith("text/html")) {
-        headers.set("content-security-policy", cspForHtml(new TextDecoder().decode(body)))
-      }
-      return new Response(body, { headers })
-    }
-
-    return Response.json({ error: "Not Found" }, { status: 404 })
-  }
-
-  const response = await proxy(upstreamURL(path), {
-    raw: request,
-    headers: ProxyUtil.headers(request, { host: UI_UPSTREAM.host }),
-  })
-  response.headers.set(
-    "Content-Security-Policy",
-    response.headers.get("content-type")?.includes("text/html") ? cspForHtml(await response.clone().text()) : csp(),
-  )
-  return response
-}
-
-export const UIRoutes = (): Hono => new Hono().all("/*", (c) => serveUI(c.req.raw))

+ 23 - 204
packages/opencode/src/server/server.ts

@@ -1,30 +1,14 @@
-import { generateSpecs } from "hono-openapi"
-import { Hono } from "hono"
-import { adapter } from "#hono"
-import { lazy } from "@/util/lazy"
 import * as Log from "@opencode-ai/core/util/log"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import { WorkspaceID } from "@/control-plane/schema"
 import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect"
 import { HttpRouter, HttpServer } from "effect/unstable/http"
 import { OpenApi } from "effect/unstable/httpapi"
 import * as HttpApiServer from "#httpapi-server"
 import { MDNS } from "./mdns"
-import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware"
-import { FenceMiddleware } from "./fence"
 import { initProjectors } from "./projectors"
-import { InstanceRoutes } from "./routes/instance"
-import { ControlPlaneRoutes } from "./routes/control"
-import { UIRoutes } from "./routes/ui"
-import { GlobalRoutes } from "./routes/global"
-import { WorkspaceRouterMiddleware } from "./workspace"
-import { InstanceMiddleware } from "./routes/instance/middleware"
-import { WorkspaceRoutes } from "./routes/control/workspace"
 import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
 import { disposeMiddleware } from "./routes/instance/httpapi/lifecycle"
 import { WebSocketTracker } from "./routes/instance/httpapi/websocket-tracker"
 import { PublicApi } from "./routes/instance/httpapi/public"
-import * as ServerBackend from "./backend"
 import type { CorsOptions } from "./cors"
 
 // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
@@ -53,203 +37,27 @@ type ListenOptions = CorsOptions & {
   mdnsDomain?: string
 }
 
-const DefaultHono = lazy(() =>
-  withBackend({ backend: "hono", reason: "stable" }, createHono({}, { backend: "hono", reason: "stable" })),
-)
-const DefaultHttpApi = lazy(() => createDefaultHttpApi())
-
-function select() {
-  return ServerBackend.select()
-}
-
-export const backend = select
-
-export const Default = () => {
-  const selected = select()
-  return selected.backend === "effect-httpapi" ? DefaultHttpApi() : DefaultHono()
-}
-
-function create(opts: ListenOptions) {
-  const selected = select()
-  return selected.backend === "effect-httpapi"
-    ? withBackend(selected, createHttpApi(opts))
-    : withBackend(selected, createHono(opts, selected))
-}
-
-export function Legacy(opts: CorsOptions = {}) {
-  return withBackend({ backend: "hono", reason: "explicit" }, createHono(opts, { backend: "hono", reason: "explicit" }))
-}
-
-function createDefaultHttpApi() {
-  return withBackend(select(), createHttpApi())
-}
-
-function withBackend<T extends { app: ServerApp; runtime: unknown }>(selection: ServerBackend.Selection, built: T) {
-  log.info("server backend selected", ServerBackend.attributes(selection))
-  return built
-}
-
-function createHttpApi(corsOptions?: CorsOptions) {
-  const handler = ExperimentalHttpApiServer.webHandler(corsOptions).handler
+const defaultHttpApi = (() => {
+  const handler = ExperimentalHttpApiServer.webHandler().handler
   const app: ServerApp = {
     fetch: (request: Request) => handler(request, ExperimentalHttpApiServer.context),
     request(input, init) {
       return app.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init))
     },
   }
-  return {
-    app,
-    runtime: adapter.createFetch(app),
-  }
-}
-
-function createHono(opts: CorsOptions, selection: ServerBackend.Selection = ServerBackend.force(select(), "hono")) {
-  const backendAttributes = ServerBackend.attributes(selection)
-  const app = new Hono()
-    .onError(ErrorMiddleware)
-    .use(CorsMiddleware(opts))
-    .use(LoggerMiddleware(backendAttributes))
-    .use(AuthMiddleware)
-    .use(CompressionMiddleware)
-    .route("/global", GlobalRoutes())
-
-  const runtime = adapter.create(app)
-
-  if (Flag.OPENCODE_WORKSPACE_ID) {
-    return {
-      app: app
-        .use(InstanceMiddleware(Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined))
-        .use(FenceMiddleware)
-        .route("/", InstanceRoutes(runtime.upgradeWebSocket, opts)),
-      runtime,
-    }
-  }
+  return { app }
+})()
 
-  const workspaceApp = new Hono()
-  const workspaceLegacyApp = new Hono()
-    .use(InstanceMiddleware())
-    .route("/experimental/workspace", WorkspaceRoutes())
-    .use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket))
-  workspaceApp.route("/", workspaceLegacyApp)
+export const Default = () => defaultHttpApi
 
-  return {
-    app: app
-      .route("/", ControlPlaneRoutes())
-      .route("/", workspaceApp)
-      .route("/", InstanceRoutes(runtime.upgradeWebSocket, opts))
-      .route("/", UIRoutes()),
-    runtime,
-  }
-}
-
-/**
- * Generate the OpenAPI document used by the SDK build.
- *
- * Since the Effect HttpApi backend now covers every Hono route (plus the new
- * `/api/session/*` v2 routes — see `httpapi-bridge.test.ts` for the parity
- * audit), `Server.openapi()` derives the spec from `OpenApi.fromApi(PublicApi)`.
- * `PublicApi` is `OpenCodeHttpApi` annotated with the `matchLegacyOpenApi`
- * transform that injects instance query parameters, strips Effect's optional
- * null arms, normalizes component names, and patches SSE response schemas so
- * the generated SDK keeps the legacy Hono shape.
- *
- * The Hono-derived spec is still reachable via `openapiHono()` so reviewers
- * can diff the two outputs while the Hono backend lingers; once the Hono
- * backend is deleted that helper goes with it.
- */
 export async function openapi() {
   return OpenApi.fromApi(PublicApi)
 }
 
-/**
- * Hono-derived OpenAPI spec, retained for parity diffing only. Delete once
- * the Hono backend is removed.
- */
-export async function openapiHono() {
-  // Build a fresh app with all routes registered directly so
-  // hono-openapi can see describeRoute metadata (`.route()` wraps
-  // handlers when the sub-app has a custom errorHandler, which
-  // strips the metadata symbol).
-  const { app } = createHono({})
-  const result = await generateSpecs(app, {
-    documentation: {
-      info: {
-        title: "opencode",
-        version: "1.0.0",
-        description: "opencode api",
-      },
-      openapi: "3.1.1",
-    },
-  })
-  return result
-}
-
 export let url: URL
 
 export async function listen(opts: ListenOptions): Promise<Listener> {
-  const selected = select()
-  const inner: Listener =
-    selected.backend === "effect-httpapi" ? await listenHttpApi(opts, selected) : await listenLegacy(opts)
-
-  const next = new URL(inner.url)
-  url = next
-
-  const mdns =
-    opts.mdns && inner.port && opts.hostname !== "127.0.0.1" && opts.hostname !== "localhost" && opts.hostname !== "::1"
-  if (mdns) {
-    MDNS.publish(inner.port, opts.mdnsDomain)
-  } else if (opts.mdns) {
-    log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
-  }
-
-  let closing: Promise<void> | undefined
-  let mdnsUnpublished = false
-  const unpublish = () => {
-    if (!mdns || mdnsUnpublished) return
-    mdnsUnpublished = true
-    MDNS.unpublish()
-  }
-  return {
-    hostname: inner.hostname,
-    port: inner.port,
-    url: next,
-    stop(close?: boolean) {
-      unpublish()
-      // Always forward stop(true), even if a graceful stop was requested
-      // first, so native listeners can escalate shutdown in-place.
-      const next = inner.stop(close)
-      closing ??= next
-      return close ? next.then(() => closing!) : closing
-    },
-  }
-}
-
-async function listenLegacy(opts: ListenOptions): Promise<Listener> {
-  const built = create(opts)
-  const server = await built.runtime.listen(opts)
-  const innerUrl = new URL("http://localhost")
-  innerUrl.hostname = opts.hostname
-  innerUrl.port = String(server.port)
-  return {
-    hostname: opts.hostname,
-    port: server.port,
-    url: innerUrl,
-    stop: (close?: boolean) => server.stop(close),
-  }
-}
-
-/**
- * Run the effect-httpapi backend on a native Effect HTTP server. This
- * lets HttpApi routes that call `request.upgrade` (PTY connect, the
- * workspace-routing proxy WS bridge) work end-to-end; the legacy Hono
- * adapter path can't surface `request.upgrade` because its fetch handler has
- * no reference to the platform server instance for websocket upgrades.
- */
-async function listenHttpApi(opts: ListenOptions, selection: ServerBackend.Selection): Promise<Listener> {
-  log.info("server backend selected", {
-    ...ServerBackend.attributes(selection),
-    "opencode.server.runtime": HttpApiServer.name,
-  })
+  log.info("server backend", { "opencode.server.runtime": HttpApiServer.name })
 
   const buildLayer = (port: number) =>
     HttpRouter.serve(ExperimentalHttpApiServer.createRoutes(opts), {
@@ -270,10 +78,6 @@ async function listenHttpApi(opts: ListenOptions, selection: ServerBackend.Selec
   const start = async (port: number) => {
     const scope = Scope.makeUnsafe()
     try {
-      // Effect's `HttpMiddleware` interface returns `Effect<…, any, any>` by
-      // design, which leaks `R = any` through `HttpRouter.serve`. The actual
-      // requirements at this point are fully satisfied by `createRoutes` and the
-      // platform HTTP server layer; cast away the `any` to satisfy `runPromise`.
       const layer = buildLayer(port) as Layer.Layer<
         HttpServer.HttpServer | WebSocketTracker.Service | HttpApiServer.Service,
         unknown,
@@ -308,8 +112,24 @@ async function listenHttpApi(opts: ListenOptions, selection: ServerBackend.Selec
   const innerUrl = new URL("http://localhost")
   innerUrl.hostname = opts.hostname
   innerUrl.port = String(port)
+  url = innerUrl
+
+  const mdns =
+    opts.mdns && port && opts.hostname !== "127.0.0.1" && opts.hostname !== "localhost" && opts.hostname !== "::1"
+  if (mdns) {
+    MDNS.publish(port, opts.mdnsDomain)
+  } else if (opts.mdns) {
+    log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
+  }
+
   let forceStopPromise: Promise<void> | undefined
   let stopPromise: Promise<void> | undefined
+  let mdnsUnpublished = false
+  const unpublish = () => {
+    if (!mdns || mdnsUnpublished) return
+    mdnsUnpublished = true
+    MDNS.unpublish()
+  }
   const forceStop = () => {
     forceStopPromise ??= Effect.runPromiseExit(
       Effect.gen(function* () {
@@ -325,9 +145,8 @@ async function listenHttpApi(opts: ListenOptions, selection: ServerBackend.Selec
     port,
     url: innerUrl,
     stop: (close?: boolean) => {
+      unpublish()
       const requested = close ? forceStop() : Promise.resolve()
-      // The first call starts scope shutdown. A later stop(true) cannot undo
-      // that, but it still runs forceStop() before awaiting the original close.
       stopPromise ??= requested
         .then(() => Effect.runPromiseExit(Scope.close(resolved!.scope, Exit.void)))
         .then(() => undefined)

+ 5 - 5
packages/opencode/src/server/shared/tui-control.ts

@@ -1,12 +1,12 @@
-import z from "zod"
 import { AsyncQueue } from "@/util/queue"
+import { Schema } from "effect"
 
-export const TuiRequest = z.object({
-  path: z.string(),
-  body: z.any(),
+export const TuiRequest = Schema.Struct({
+  path: Schema.String,
+  body: Schema.Unknown,
 })
 
-export type TuiRequest = z.infer<typeof TuiRequest>
+export type TuiRequest = Schema.Schema.Type<typeof TuiRequest>
 
 const request = new AsyncQueue<TuiRequest>()
 const response = new AsyncQueue<unknown>()

+ 0 - 93
packages/opencode/src/server/workspace.ts

@@ -1,93 +0,0 @@
-import type { MiddlewareHandler } from "hono"
-import type { UpgradeWebSocket } from "hono/ws"
-import { getAdapter } from "@/control-plane/adapters"
-import { WorkspaceID } from "@/control-plane/schema"
-import { WorkspaceContext } from "@/control-plane/workspace-context"
-import { Workspace } from "@/control-plane/workspace"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import { AppRuntime } from "@/effect/app-runtime"
-import { WithInstance } from "@/project/with-instance"
-import { Session } from "@/session/session"
-import { Effect } from "effect"
-import * as Log from "@opencode-ai/core/util/log"
-import { ServerProxy } from "./proxy"
-import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "./shared/workspace-routing"
-
-async function getSessionWorkspace(url: URL) {
-  const id = getWorkspaceRouteSessionID(url)
-  if (!id) return null
-
-  const session = await AppRuntime.runPromise(
-    Session.Service.use((svc) => svc.get(id)).pipe(Effect.withSpan("WorkspaceRouter.lookup")),
-  ).catch(() => undefined)
-  return session?.workspaceID
-}
-
-export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
-  const log = Log.create({ service: "workspace-router" })
-
-  return async (c, next) => {
-    const url = new URL(c.req.url)
-
-    const sessionWorkspaceID = await getSessionWorkspace(url)
-    const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace")
-
-    if (!workspaceID || url.pathname.startsWith("/console") || Flag.OPENCODE_WORKSPACE_ID) {
-      return next()
-    }
-
-    const workspace = await AppRuntime.runPromise(
-      Workspace.Service.use((svc) => svc.get(WorkspaceID.make(workspaceID))),
-    )
-
-    if (!workspace) {
-      return new Response(`Workspace not found: ${workspaceID}`, {
-        status: 500,
-        headers: {
-          "content-type": "text/plain; charset=utf-8",
-        },
-      })
-    }
-
-    if (isLocalWorkspaceRoute(c.req.method, url.pathname)) {
-      // No instance provided because we are serving cached data; there
-      // is no instance to work with
-      return next()
-    }
-
-    const adapter = getAdapter(workspace.projectID, workspace.type)
-    const target = await adapter.target(workspace)
-
-    if (target.type === "local") {
-      return WorkspaceContext.provide({
-        workspaceID: WorkspaceID.make(workspaceID),
-        fn: () =>
-          WithInstance.provide({
-            directory: target.directory,
-            async fn() {
-              return next()
-            },
-          }),
-      })
-    }
-
-    const proxyURL = workspaceProxyURL(target.url, url)
-
-    log.info("workspace proxy forwarding", {
-      workspaceID,
-      request: url.toString(),
-      target: String(target.url),
-      proxy: proxyURL.toString(),
-    })
-
-    if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
-      return ServerProxy.websocket(upgrade, proxyURL, target.headers, c.req.raw, c.env)
-    }
-
-    const headers = new Headers(c.req.raw.headers)
-    headers.delete("x-opencode-workspace")
-
-    const req = new Request(c.req.raw, { headers })
-    return ServerProxy.http(proxyURL, target.headers, req, workspace.id)
-  }
-}

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

@@ -446,19 +446,6 @@ export type Part =
   | RetryPart
   | CompactionPart
 
-// Zod discriminated union kept for the legacy Hono OpenAPI path.
-const AssistantErrorZod = z.discriminatedUnion("name", [
-  AuthError.Schema,
-  NamedError.Unknown.Schema,
-  OutputLengthError.Schema,
-  AbortedError.Schema,
-  StructuredOutputError.Schema,
-  ContextOverflowError.Schema,
-  APIError.Schema,
-])
-type AssistantError = z.infer<typeof AssistantErrorZod>
-
-// Effect Schema for the same union — used by HttpApi OpenAPI generation.
 const AssistantErrorSchema = Schema.Union([
   AuthError.EffectSchema,
   Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({
@@ -470,6 +457,7 @@ const AssistantErrorSchema = Schema.Union([
   ContextOverflowError.EffectSchema,
   APIError.EffectSchema,
 ]).annotate({ discriminator: "name" })
+type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
 
 // ── Prompt input schemas ─────────────────────────────────────────────────────
 //

+ 0 - 22
packages/opencode/src/sync/index.ts

@@ -1,4 +1,3 @@
-import z from "zod"
 import { Database } from "@/storage/db"
 import { eq } from "drizzle-orm"
 import { GlobalBus } from "@/bus/global"
@@ -10,7 +9,6 @@ import type { WorkspaceID } from "@/control-plane/schema"
 import { EventID } from "./schema"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { Context, Effect, Layer, Schema as EffectSchema } from "effect"
-import { zodObject } from "@/util/effect-zod"
 import type { DeepMutable } from "@/util/schema"
 import { makeRuntime } from "@/effect/run-service"
 import { serviceUse } from "@/effect/service-use"
@@ -364,26 +362,6 @@ export function claim(aggregateID: string, ownerID: string) {
   )
 }
 
-export function payloads() {
-  return registry
-    .entries()
-    .map(([type, def]) => {
-      return z
-        .object({
-          type: z.literal("sync"),
-          name: z.literal(type),
-          id: z.string(),
-          seq: z.number(),
-          aggregateID: z.literal(def.aggregate),
-          data: zodObject(def.schema),
-        })
-        .meta({
-          ref: `SyncEvent.${def.type}`,
-        })
-    })
-    .toArray()
-}
-
 export function effectPayloads() {
   return registry
     .entries()

+ 4 - 14
packages/opencode/src/worktree/index.ts

@@ -21,8 +21,6 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
 import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { InstanceState } from "@/effect/instance-state"
-import { zod as effectZod } from "@/util/effect-zod"
-import { withStatics } from "@/util/schema"
 
 const log = Log.create({ service: "worktree" })
 
@@ -46,9 +44,7 @@ export const Info = Schema.Struct({
   name: Schema.String,
   branch: Schema.String,
   directory: Schema.String,
-})
-  .annotate({ identifier: "Worktree" })
-  .pipe(withStatics((s) => ({ zod: effectZod(s) })))
+}).annotate({ identifier: "Worktree" })
 export type Info = Schema.Schema.Type<typeof Info>
 
 export const CreateInput = Schema.Struct({
@@ -56,23 +52,17 @@ export const CreateInput = Schema.Struct({
   startCommand: Schema.optional(
     Schema.String.annotate({ description: "Additional startup script to run after the project's start command" }),
   ),
-})
-  .annotate({ identifier: "WorktreeCreateInput" })
-  .pipe(withStatics((s) => ({ zod: effectZod(s) })))
+}).annotate({ identifier: "WorktreeCreateInput" })
 export type CreateInput = Schema.Schema.Type<typeof CreateInput>
 
 export const RemoveInput = Schema.Struct({
   directory: Schema.String,
-})
-  .annotate({ identifier: "WorktreeRemoveInput" })
-  .pipe(withStatics((s) => ({ zod: effectZod(s) })))
+}).annotate({ identifier: "WorktreeRemoveInput" })
 export type RemoveInput = Schema.Schema.Type<typeof RemoveInput>
 
 export const ResetInput = Schema.Struct({
   directory: Schema.String,
-})
-  .annotate({ identifier: "WorktreeResetInput" })
-  .pipe(withStatics((s) => ({ zod: effectZod(s) })))
+}).annotate({ identifier: "WorktreeResetInput" })
 export type ResetInput = Schema.Schema.Type<typeof ResetInput>
 
 export const NotGitError = NamedError.create(

+ 5 - 4
packages/opencode/test/control-plane/workspace.test.ts

@@ -5,7 +5,7 @@ import Http from "node:http"
 import path from "node:path"
 import { setTimeout as delay } from "node:timers/promises"
 import { NodeHttpServer } from "@effect/platform-node"
-import { Effect, Layer } from "effect"
+import { Effect, Layer, Schema } from "effect"
 import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
 import { eq } from "drizzle-orm"
 import * as Log from "@opencode-ai/core/util/log"
@@ -376,9 +376,10 @@ describe("workspace schemas and exports", () => {
       extra: { nested: true },
     }
 
-    expect(Workspace.CreateInput.zod.parse(input)).toEqual(input)
-    expect(() => Workspace.CreateInput.zod.parse({ ...input, id: "bad" })).toThrow()
-    expect(() => Workspace.CreateInput.zod.parse({ ...input, branch: 1 })).toThrow()
+    const decode = Schema.decodeUnknownSync(Workspace.CreateInput)
+    expect(decode(input)).toEqual(input)
+    expect(() => decode({ ...input, id: 1 })).toThrow()
+    expect(() => decode({ ...input, branch: 1 })).toThrow()
   })
 })
 

+ 0 - 86
packages/opencode/test/project/instance-bootstrap-regression.test.ts

@@ -1,86 +0,0 @@
-import { afterEach, expect, test } from "bun:test"
-import { Hono } from "hono"
-import { existsSync } from "node:fs"
-import path from "node:path"
-import { pathToFileURL } from "node:url"
-import { bootstrap as cliBootstrap } from "../../src/cli/bootstrap"
-import { Instance } from "../../src/project/instance"
-import { WithInstance } from "../../src/project/with-instance"
-import { InstanceRuntime } from "../../src/project/instance-runtime"
-import { InstanceMiddleware } from "../../src/server/routes/instance/middleware"
-import { disposeAllInstances, tmpdir } from "../fixture/fixture"
-
-// These regressions cover the legacy instance-loading paths fixed by PRs
-// #25389 and #25449. The plugin config hook writes a marker file, and the test
-// bodies deliberately avoid touching Plugin or config directly. The marker only
-// exists if InstanceBootstrap ran at the instance boundary.
-
-afterEach(async () => {
-  await disposeAllInstances()
-})
-
-async function bootstrapFixture() {
-  return tmpdir({
-    init: async (dir) => {
-      const marker = path.join(dir, "config-hook-fired")
-      const pluginFile = path.join(dir, "plugin.ts")
-      await Bun.write(
-        pluginFile,
-        [
-          `const MARKER = ${JSON.stringify(marker)}`,
-          "export default async () => ({",
-          "  config: async () => {",
-          '    await Bun.write(MARKER, "ran")',
-          "  },",
-          "})",
-          "",
-        ].join("\n"),
-      )
-      await Bun.write(
-        path.join(dir, "opencode.json"),
-        JSON.stringify({
-          $schema: "https://opencode.ai/config.json",
-          plugin: [pathToFileURL(pluginFile).href],
-        }),
-      )
-      return marker
-    },
-  })
-}
-
-test("Instance.provide runs InstanceBootstrap before fn (boundary invariant)", async () => {
-  await using tmp = await bootstrapFixture()
-
-  await WithInstance.provide({
-    directory: tmp.path,
-    fn: async () => "ok",
-  })
-
-  expect(existsSync(tmp.extra)).toBe(true)
-})
-
-test("CLI bootstrap runs InstanceBootstrap before callback", async () => {
-  await using tmp = await bootstrapFixture()
-
-  await cliBootstrap(tmp.path, async () => "ok")
-
-  expect(existsSync(tmp.extra)).toBe(true)
-})
-
-test("legacy Hono instance middleware runs InstanceBootstrap before next handler", async () => {
-  await using tmp = await bootstrapFixture()
-  const app = new Hono().use(InstanceMiddleware()).get("/probe", (c) => c.text("ok"))
-
-  const response = await app.request("/probe", { headers: { "x-opencode-directory": tmp.path } })
-
-  expect(response.status).toBe(200)
-  expect(existsSync(tmp.extra)).toBe(true)
-})
-
-test("InstanceRuntime.reloadInstance runs InstanceBootstrap", async () => {
-  await using tmp = await bootstrapFixture()
-
-  await InstanceRuntime.reloadInstance({ directory: tmp.path })
-
-  expect(existsSync(tmp.extra)).toBe(true)
-})

+ 0 - 501
packages/opencode/test/server/httpapi-bridge.test.ts

@@ -1,501 +0,0 @@
-import { afterEach, describe, expect, test } from "bun:test"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import { Instance } from "../../src/project/instance"
-import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/control"
-import { FilePaths } from "../../src/server/routes/instance/httpapi/groups/file"
-import { GlobalPaths } from "../../src/server/routes/instance/httpapi/groups/global"
-import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
-import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
-import { Server } from "../../src/server/server"
-import * as Log from "@opencode-ai/core/util/log"
-import { ConfigProvider, Layer } from "effect"
-import { HttpRouter } from "effect/unstable/http"
-import { OpenApi } from "effect/unstable/httpapi"
-import { resetDatabase } from "../fixture/db"
-import { disposeAllInstances, tmpdir } from "../fixture/fixture"
-
-void Log.init({ print: false })
-
-const original = {
-  OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
-  OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
-  OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
-}
-
-const methods = ["get", "post", "put", "delete", "patch"] as const
-let effectSpec: ReturnType<typeof OpenApi.fromApi> | undefined
-
-function effectOpenApi() {
-  return (effectSpec ??= OpenApi.fromApi(PublicApi))
-}
-
-function app(input?: { password?: string; username?: string }) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
-  Flag.OPENCODE_SERVER_PASSWORD = input?.password
-  Flag.OPENCODE_SERVER_USERNAME = input?.username
-
-  const handler = HttpRouter.toWebHandler(
-    ExperimentalHttpApiServer.routes.pipe(
-      Layer.provide(
-        ConfigProvider.layer(
-          ConfigProvider.fromUnknown({
-            OPENCODE_SERVER_PASSWORD: input?.password,
-            OPENCODE_SERVER_USERNAME: input?.username,
-          }),
-        ),
-      ),
-    ),
-    { disableLogger: true },
-  ).handler
-  return {
-    fetch: (request: Request) => handler(request, ExperimentalHttpApiServer.context),
-    request(input: string | URL | Request, init?: RequestInit) {
-      return this.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init))
-    },
-  }
-}
-
-function openApiRouteKeys(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], unknown>>> }) {
-  return Object.entries(spec.paths)
-    .flatMap(([path, item]) =>
-      methods.filter((method) => item[method]).map((method) => `${method.toUpperCase()} ${path}`),
-    )
-    .sort()
-}
-
-function openApiParameters(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }) {
-  return Object.fromEntries(
-    Object.entries(spec.paths).flatMap(([path, item]) =>
-      methods
-        .filter((method) => item[method])
-        .map((method) => [
-          `${method.toUpperCase()} ${path}`,
-          (item[method]?.parameters ?? [])
-            .map(parameterKey)
-            .filter((param) => param !== undefined)
-            .sort(),
-        ]),
-    ),
-  )
-}
-
-function openApiRequestBodies(spec: OpenApiSpec) {
-  return Object.fromEntries(
-    Object.entries(spec.paths).flatMap(([path, item]) =>
-      methods
-        .filter((method) => item[method])
-        .map((method) => [`${method.toUpperCase()} ${path}`, requestBodyKey(spec, item[method]?.requestBody)]),
-    ),
-  )
-}
-
-type OpenApiSpec = {
-  components?: {
-    schemas?: Record<string, unknown>
-  }
-  paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>>
-}
-
-type OpenApiSchema = {
-  $ref?: string
-  allOf?: unknown[]
-  anyOf?: unknown[]
-  oneOf?: unknown[]
-  properties?: Record<string, unknown>
-  type?: string | string[]
-}
-
-type Operation = {
-  parameters?: unknown[]
-  responses?: unknown
-  requestBody?: unknown
-}
-
-type RequestBody = {
-  content?: Record<string, { schema?: OpenApiSchema }>
-  required?: boolean
-}
-
-function parameterKey(param: unknown): string | undefined {
-  if (!param || typeof param !== "object" || !("in" in param) || !("name" in param)) return undefined
-  if (typeof param.in !== "string" || typeof param.name !== "string") return undefined
-  return `${param.in}:${param.name}:${"required" in param && param.required === true}:${stableSchema(
-    "schema" in param ? param.schema : undefined,
-  )}`
-}
-
-function stableSchema(input: unknown): string {
-  return JSON.stringify(sortSchema(input))
-}
-
-function sortSchema(input: unknown): unknown {
-  if (Array.isArray(input)) return input.map(sortSchema)
-  if (!input || typeof input !== "object") return input
-  return Object.fromEntries(
-    Object.entries(input)
-      .sort(([left], [right]) => left.localeCompare(right))
-      .map(([key, value]) => [key, sortSchema(value)]),
-  )
-}
-
-function parameterSchema(input: {
-  spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }
-  path: string
-  method: (typeof methods)[number]
-  name: string
-}): unknown {
-  const param = input.spec.paths[input.path]?.[input.method]?.parameters?.find(
-    (param) => !!param && typeof param === "object" && "name" in param && param.name === input.name,
-  )
-  if (!param || typeof param !== "object" || !("schema" in param)) return undefined
-  return param.schema
-}
-
-function requestBodyKey(spec: OpenApiSpec, body: unknown) {
-  if (!body || typeof body !== "object" || !("content" in body)) return ""
-  // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Guarded above; test helper only needs this OpenAPI subset.
-  const requestBody = body as RequestBody
-  return JSON.stringify({
-    required: requestBody.required === true,
-    content: Object.entries(requestBody.content ?? {})
-      .map(([type, value]) => [type, requestBodySchemaKind(spec, value.schema)] as const)
-      .sort(([left], [right]) => left.localeCompare(right)),
-  })
-}
-
-function requestBodySchemaKind(spec: OpenApiSpec, schema: OpenApiSchema | undefined) {
-  if (!schema) return ""
-  // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- `$ref` lookup is constrained to OpenAPI schema components in this test helper.
-  const resolved = (
-    schema.$ref ? spec.components?.schemas?.[schema.$ref.replace("#/components/schemas/", "")] : schema
-  ) as OpenApiSchema | undefined
-  if (resolved?.properties) return "object"
-  if (resolved?.anyOf ?? resolved?.oneOf ?? resolved?.allOf) return "object"
-  return resolved?.type ?? schema.type ?? "inline"
-}
-
-function responseContentTypes(input: {
-  spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }
-  path: string
-  method: (typeof methods)[number]
-  status: string
-}) {
-  const responses = input.spec.paths[input.path]?.[input.method]?.responses
-  if (!responses || typeof responses !== "object" || !(input.status in responses)) return []
-  // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Guarded dynamic OpenAPI response lookup.
-  const response = (responses as Record<string, unknown>)[input.status]
-  if (!response || typeof response !== "object" || !("content" in response)) return []
-  const content = (response as { content?: unknown }).content
-  if (!content || typeof content !== "object") {
-    return []
-  }
-  return Object.keys(content).sort()
-}
-
-function authorization(username: string, password: string) {
-  return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
-}
-
-function fileUrl(input?: { directory?: string; token?: string }) {
-  const url = new URL(`http://localhost${FilePaths.content}`)
-  url.searchParams.set("path", "hello.txt")
-  if (input?.directory) url.searchParams.set("directory", input.directory)
-  if (input?.token) url.searchParams.set("auth_token", input.token)
-  return url
-}
-
-afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
-  Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
-  Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
-  await disposeAllInstances()
-  await resetDatabase()
-})
-
-describe("HttpApi server", () => {
-  test("keeps Effect HttpApi behind the feature flag", () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
-    expect(Server.backend()).toEqual({ backend: "hono", reason: "stable" })
-
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
-    expect(Server.backend()).toEqual({ backend: "effect-httpapi", reason: "env" })
-  })
-
-  test("covers every generated OpenAPI route with Effect HttpApi contracts", async () => {
-    const honoRoutes = openApiRouteKeys(await Server.openapiHono())
-    const effectRoutes = openApiRouteKeys(effectOpenApi())
-
-    expect(honoRoutes.filter((route) => !effectRoutes.includes(route))).toEqual([])
-    expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([
-      "GET /api/session",
-      "GET /api/session/{sessionID}/context",
-      "GET /api/session/{sessionID}/message",
-      "POST /api/session/{sessionID}/compact",
-      "POST /api/session/{sessionID}/prompt",
-      "POST /api/session/{sessionID}/wait",
-    ])
-  })
-
-  test("matches generated OpenAPI route parameters", async () => {
-    const hono = openApiParameters(await Server.openapiHono())
-    const effect = openApiParameters(effectOpenApi())
-
-    expect(
-      Object.keys(hono)
-        .filter((route) => JSON.stringify(hono[route]) !== JSON.stringify(effect[route]))
-        .map((route) => ({ route, hono: hono[route], effect: effect[route] })),
-    ).toEqual([])
-  })
-
-  test("matches generated OpenAPI request body shape", async () => {
-    const hono = openApiRequestBodies(await Server.openapiHono())
-    const effect = openApiRequestBodies(effectOpenApi())
-
-    expect(
-      Object.keys(hono)
-        .filter((route) => hono[route] !== effect[route])
-        .map((route) => ({ route, hono: hono[route], effect: effect[route] })),
-    ).toEqual([])
-  })
-
-  test("matches SDK-affecting query parameter schemas", async () => {
-    const effect = effectOpenApi()
-
-    expect(parameterSchema({ spec: effect, path: "/session", method: "get", name: "roots" })).toEqual({
-      anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
-    })
-    expect(parameterSchema({ spec: effect, path: "/session", method: "get", name: "start" })).toEqual({
-      type: "number",
-    })
-    expect(parameterSchema({ spec: effect, path: "/find/file", method: "get", name: "limit" })).toEqual({
-      type: "integer",
-      minimum: 1,
-      maximum: 200,
-    })
-    expect(
-      parameterSchema({ spec: effect, path: "/session/{sessionID}/message", method: "get", name: "limit" }),
-    ).toEqual({
-      type: "integer",
-      minimum: 0,
-      maximum: Number.MAX_SAFE_INTEGER,
-    })
-  })
-
-  test("matches SDK-affecting request schema details", () => {
-    const effect = effectOpenApi()
-    const sessionUpdate = effect.paths["/session/{sessionID}"]?.patch?.requestBody
-    const sessionUpdateSchema =
-      typeof sessionUpdate === "object" && sessionUpdate && "content" in sessionUpdate
-        ? sessionUpdate.content?.["application/json"]?.schema
-        : undefined
-    const sessionUpdateProperties = sessionUpdateSchema?.properties as Record<string, OpenApiSchema> | undefined
-    const time = sessionUpdateProperties?.time
-    expect(time?.properties?.archived).toEqual({ type: "number" })
-  })
-
-  test("documents event routes as server-sent events", () => {
-    const effect = effectOpenApi()
-
-    expect(responseContentTypes({ spec: effect, path: "/event", method: "get", status: "200" })).toEqual([
-      "text/event-stream",
-    ])
-    expect(responseContentTypes({ spec: effect, path: "/global/event", method: "get", status: "200" })).toEqual([
-      "text/event-stream",
-    ])
-  })
-
-  test("allows requests when auth is disabled", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await Bun.write(`${tmp.path}/hello.txt`, "hello")
-
-    const response = await app().request(fileUrl(), {
-      headers: {
-        "x-opencode-directory": tmp.path,
-      },
-    })
-
-    expect(response.status).toBe(200)
-    expect(await response.json()).toMatchObject({ content: "hello" })
-  })
-
-  test("provides instance context to bridged handlers", async () => {
-    await using tmp = await tmpdir({ git: true })
-
-    const response = await app().request("/project/current", {
-      headers: {
-        "x-opencode-directory": tmp.path,
-      },
-    })
-
-    expect(response.status).toBe(200)
-    expect(await response.json()).toMatchObject({ worktree: tmp.path })
-  })
-
-  test("requires credentials when auth is enabled", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await Bun.write(`${tmp.path}/hello.txt`, "hello")
-
-    const [missing, bad, good] = await Promise.all([
-      app({ password: "secret" }).request(fileUrl(), {
-        headers: { "x-opencode-directory": tmp.path },
-      }),
-      app({ password: "secret" }).request(fileUrl(), {
-        headers: {
-          authorization: authorization("opencode", "wrong"),
-          "x-opencode-directory": tmp.path,
-        },
-      }),
-      app({ password: "secret" }).request(fileUrl(), {
-        headers: {
-          authorization: authorization("opencode", "secret"),
-          "x-opencode-directory": tmp.path,
-        },
-      }),
-    ])
-
-    expect(missing.status).toBe(401)
-    expect(bad.status).toBe(401)
-    expect(good.status).toBe(200)
-  })
-
-  test("requires credentials for root routes when auth is enabled", async () => {
-    const server = app({ password: "secret" })
-    const auth = { authorization: authorization("opencode", "secret") }
-    const wrongAuth = { authorization: authorization("opencode", "wrong") }
-
-    const [missingHealth, goodHealth, missingConfig, wrongConfig, goodConfig] = await Promise.all([
-      server.request(GlobalPaths.health),
-      server.request(GlobalPaths.health, { headers: auth }),
-      server.request(GlobalPaths.config),
-      server.request(GlobalPaths.config, { headers: wrongAuth }),
-      server.request(GlobalPaths.config, { headers: auth }),
-    ])
-
-    expect(missingHealth.status).toBe(401)
-    expect(goodHealth.status).toBe(200)
-    expect(missingConfig.status).toBe(401)
-    expect(wrongConfig.status).toBe(401)
-    expect(goodConfig.status).toBe(200)
-
-    const missingDispose = await server.request(GlobalPaths.dispose, { method: "POST" })
-    expect(missingDispose.status).toBe(401)
-
-    const missingUpgrade = await server.request(GlobalPaths.upgrade, {
-      method: "POST",
-      headers: { "content-type": "application/json" },
-      body: "not-json",
-    })
-    expect(missingUpgrade.status).toBe(401)
-
-    const invalidUpgrade = await server.request(GlobalPaths.upgrade, {
-      method: "POST",
-      headers: { ...auth, "content-type": "application/json" },
-      body: "not-json",
-    })
-    expect(invalidUpgrade.status).toBe(400)
-
-    const missingLog = await server.request(ControlPaths.log, {
-      method: "POST",
-      headers: { "content-type": "application/json" },
-      body: JSON.stringify({ service: "httpapi-auth-test", level: "info", message: "hello" }),
-    })
-    expect(missingLog.status).toBe(401)
-
-    const missingAuth = await server.request(ControlPaths.auth.replace(":providerID", "test"), {
-      method: "PUT",
-      headers: { "content-type": "application/json" },
-      body: JSON.stringify({ type: "api", key: "secret" }),
-    })
-    expect(missingAuth.status).toBe(401)
-
-    const invalidAuth = await server.request(ControlPaths.auth.replace(":providerID", "test"), {
-      method: "PUT",
-      headers: { ...auth, "content-type": "application/json" },
-      body: JSON.stringify({ type: "api" }),
-    })
-    expect(invalidAuth.status).toBe(400)
-  })
-
-  test("accepts auth_token query credentials", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await Bun.write(`${tmp.path}/hello.txt`, "hello")
-
-    const response = await app({ password: "secret" }).request(
-      fileUrl({ token: Buffer.from("opencode:secret").toString("base64") }),
-      {
-        headers: {
-          "x-opencode-directory": tmp.path,
-        },
-      },
-    )
-
-    expect(response.status).toBe(200)
-  })
-
-  test("selects instance from query before directory header", async () => {
-    await using header = await tmpdir({ git: true })
-    await using query = await tmpdir({ git: true })
-    await Bun.write(`${header.path}/hello.txt`, "header")
-    await Bun.write(`${query.path}/hello.txt`, "query")
-
-    const response = await app().request(fileUrl({ directory: query.path }), {
-      headers: {
-        "x-opencode-directory": header.path,
-      },
-    })
-
-    expect(response.status).toBe(200)
-    expect(await response.json()).toMatchObject({ content: "query" })
-  })
-
-  test("serves global health from Effect HttpApi", async () => {
-    const response = await app().request(`${GlobalPaths.health}?directory=/does/not/exist/opencode-test`)
-
-    expect(response.status).toBe(200)
-    expect(await response.json()).toMatchObject({ healthy: true })
-  })
-
-  test("serves global event stream from Effect HttpApi", async () => {
-    const response = await app().request(GlobalPaths.event)
-    if (!response.body) throw new Error("missing event stream body")
-    const reader = response.body.getReader()
-    const chunk = await reader.read()
-    await reader.cancel()
-
-    expect(response.status).toBe(200)
-    expect(response.headers.get("content-type")).toContain("text/event-stream")
-    expect(new TextDecoder().decode(chunk.value)).toContain("server.connected")
-  })
-
-  test("serves control log from Effect HttpApi", async () => {
-    const response = await app().request(ControlPaths.log, {
-      method: "POST",
-      headers: { "content-type": "application/json" },
-      body: JSON.stringify({ service: "httpapi-test", level: "info", message: "hello" }),
-    })
-
-    expect(response.status).toBe(200)
-    expect(await response.json()).toBe(true)
-  })
-
-  test("validates control auth without falling through to 404", async () => {
-    const response = await app().request(ControlPaths.auth.replace(":providerID", "test"), {
-      method: "PUT",
-      headers: { "content-type": "application/json" },
-      body: JSON.stringify({ type: "api" }),
-    })
-
-    expect(response.status).toBe(400)
-  })
-
-  test("validates global upgrade without invoking installers", async () => {
-    const response = await app().request(GlobalPaths.upgrade, {
-      method: "POST",
-      headers: { "content-type": "application/json" },
-      body: "not-json",
-    })
-
-    expect(response.status).toBe(400)
-    expect(await response.json()).toMatchObject({ success: false })
-  })
-})

+ 0 - 5
packages/opencode/test/server/httpapi-compression.test.ts

@@ -1,6 +1,5 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import { gunzipSync, inflateSync } from "node:zlib"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import * as Log from "@opencode-ai/core/util/log"
 import { Server } from "../../src/server/server"
 import { resetDatabase } from "../fixture/db"
@@ -8,16 +7,12 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
-
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   await disposeAllInstances()
   await resetDatabase()
 })
 
 function app() {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
   return Server.Default().app
 }
 

+ 0 - 4
packages/opencode/test/server/httpapi-config.test.ts

@@ -1,6 +1,5 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import path from "path"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { Server } from "../../src/server/server"
 import * as Log from "@opencode-ai/core/util/log"
 import { resetDatabase } from "../fixture/db"
@@ -9,10 +8,8 @@ import { waitGlobalBusEventPromise } from "./global-bus"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 
 function app() {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
   return Server.Default().app
 }
 
@@ -24,7 +21,6 @@ async function waitDisposed(directory: string) {
 }
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   await disposeAllInstances()
   await resetDatabase()
 })

+ 7 - 23
packages/opencode/test/server/httpapi-cors-vary.test.ts

@@ -1,5 +1,4 @@
 import { afterEach, describe, expect, test } from "bun:test"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import * as Log from "@opencode-ai/core/util/log"
 import { Server } from "../../src/server/server"
 import { resetDatabase } from "../fixture/db"
@@ -7,17 +6,13 @@ import { disposeAllInstances } from "../fixture/fixture"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
-
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   await disposeAllInstances()
   await resetDatabase()
 })
 
-function app(experimental: boolean) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
+function app() {
+  return Server.Default().app
 }
 
 const PREFLIGHT_HEADERS = {
@@ -33,19 +28,8 @@ const PREFLIGHT_HEADERS = {
 // cached for one origin against a different origin. corsVaryFixLayer
 // restores the merged form.
 describe("CORS preflight Vary header", () => {
-  test("Hono backend preflight Vary contains Origin", async () => {
-    const response = await app(false).request("/global/config", {
-      method: "OPTIONS",
-      headers: PREFLIGHT_HEADERS,
-    })
-
-    expect([200, 204]).toContain(response.status)
-    expect(response.headers.get("access-control-allow-origin")).toBe("http://localhost:3000")
-    expect((response.headers.get("vary") ?? "").toLowerCase()).toContain("origin")
-  })
-
   test("HTTP API backend preflight Vary contains Origin", async () => {
-    const response = await app(true).request("/global/config", {
+    const response = await app().request("/global/config", {
       method: "OPTIONS",
       headers: PREFLIGHT_HEADERS,
     })
@@ -56,7 +40,7 @@ describe("CORS preflight Vary header", () => {
   })
 
   test("HTTP API backend preflight Vary still preserves Access-Control-Request-Headers", async () => {
-    const response = await app(true).request("/global/config", {
+    const response = await app().request("/global/config", {
       method: "OPTIONS",
       headers: PREFLIGHT_HEADERS,
     })
@@ -67,7 +51,7 @@ describe("CORS preflight Vary header", () => {
   })
 
   test("HTTP API backend does not duplicate Origin in Vary", async () => {
-    const response = await app(true).request("/global/config", {
+    const response = await app().request("/global/config", {
       method: "OPTIONS",
       headers: PREFLIGHT_HEADERS,
     })
@@ -75,8 +59,8 @@ describe("CORS preflight Vary header", () => {
     const vary = response.headers.get("vary") ?? ""
     const originCount = vary
       .split(",")
-      .map((s) => s.trim().toLowerCase())
-      .filter((s) => s === "origin").length
+      .map((s: string) => s.trim().toLowerCase())
+      .filter((s: string) => s === "origin").length
     expect(originCount).toBe(1)
   })
 })

+ 15 - 9
packages/opencode/test/server/httpapi-cors.test.ts

@@ -1,7 +1,7 @@
 import { NodeHttpServer, NodeServices } from "@effect/platform-node"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { describe, expect } from "bun:test"
-import { Config, Effect, Layer } from "effect"
+import { Config, ConfigProvider, Effect, Layer } from "effect"
 import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
 import * as Socket from "effect/unstable/socket/Socket"
 import { Server } from "../../src/server/server"
@@ -13,15 +13,12 @@ import { testEffect } from "../lib/effect"
 const testStateLayer = Layer.effectDiscard(
   Effect.gen(function* () {
     const original = {
-      OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
       OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
     }
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_SERVER_PASSWORD = "secret"
     yield* Effect.promise(() => resetDatabase())
     yield* Effect.addFinalizer(() =>
       Effect.promise(async () => {
-        Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
         Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
         await resetDatabase()
       }),
@@ -63,12 +60,21 @@ describe("HttpApi CORS", () => {
     }),
   )
 
-  it.live("adds CORS headers to legacy unauthorized responses", () =>
+  it.live("adds CORS headers to unauthorized responses", () =>
     Effect.gen(function* () {
-      const response = yield* Effect.promise(async () =>
-        Server.Legacy().app.request("/global/config", {
-          headers: { origin: "https://app.opencode.ai" },
-        }),
+      const handler = HttpRouter.toWebHandler(
+        ExperimentalHttpApiServer.createRoutes().pipe(
+          Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({ OPENCODE_SERVER_PASSWORD: "secret" }))),
+        ),
+        { disableLogger: true },
+      ).handler
+      const response = yield* Effect.promise(() =>
+        handler(
+          new Request(new URL("/global/config", "http://localhost"), {
+            headers: { origin: "https://app.opencode.ai" },
+          }),
+          ExperimentalHttpApiServer.context,
+        ),
       )
 
       expect(response.status).toBe(401)

+ 7 - 16
packages/opencode/test/server/httpapi-event.test.ts

@@ -1,5 +1,4 @@
 import { afterEach, describe, expect, test } from "bun:test"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { Instance } from "../../src/project/instance"
 import { Server } from "../../src/server/server"
 import { EventPaths } from "../../src/server/routes/instance/httpapi/event"
@@ -9,11 +8,8 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
-
-function app(experimental = true) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
+function app() {
+  return Server.Default().app
 }
 
 async function readFirstChunk(response: Response) {
@@ -36,13 +32,12 @@ async function readFirstEvent(response: Response) {
 }
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   await disposeAllInstances()
   await resetDatabase()
 })
 
-describe("event HttpApi bridge", () => {
-  test("serves event stream through experimental Effect route", async () => {
+describe("event HttpApi", () => {
+  test("serves event stream", async () => {
     await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
     const response = await app().request(EventPaths.event, { headers: { "x-opencode-directory": tmp.path } })
 
@@ -54,15 +49,11 @@ describe("event HttpApi bridge", () => {
     expect(await readFirstEvent(response)).toMatchObject({ type: "server.connected", properties: {} })
   })
 
-  test("matches legacy first event frame", async () => {
+  test("serves the initial server connected event", async () => {
     await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
     const headers = { "x-opencode-directory": tmp.path }
-    const legacy = await app(false).request(EventPaths.event, { headers })
-    const effect = await app(true).request(EventPaths.event, { headers })
+    const response = await app().request(EventPaths.event, { headers })
 
-    const legacyEvent = await readFirstEvent(legacy)
-    const effectEvent = await readFirstEvent(effect)
-    expect(effectEvent.type).toBe(legacyEvent.type)
-    expect(effectEvent.properties).toEqual(legacyEvent.properties)
+    expect(await readFirstEvent(response)).toMatchObject({ type: "server.connected", properties: {} })
   })
 })

+ 0 - 10
packages/opencode/test/server/httpapi-exercise/backend.ts

@@ -1,4 +1,3 @@
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { ConfigProvider, Effect, Layer } from "effect"
 import { HttpRouter } from "effect/unstable/http"
 import { parse } from "./assertions"
@@ -56,16 +55,7 @@ function app(modules: Runtime, backend: Backend, options: CallOptions) {
   const username = options.auth?.username
   const password = options.auth?.password
   const cacheKey = `${backend}:${username ?? ""}:${password ?? ""}`
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "effect"
-  Flag.OPENCODE_SERVER_PASSWORD = password
-  Flag.OPENCODE_SERVER_USERNAME = username
   if (appCache[cacheKey]) return appCache[cacheKey]
-  if (backend === "legacy") {
-    const legacy = modules.Server.Legacy().app
-    return (appCache[cacheKey] = {
-      request: (input, init) => legacy.request(input, init),
-    })
-  }
 
   const handler = HttpRouter.toWebHandler(
     modules.ExperimentalHttpApiServer.routes.pipe(

+ 0 - 1
packages/opencode/test/server/httpapi-exercise/environment.ts

@@ -22,7 +22,6 @@ process.env.OPENCODE_DB = exerciseDatabasePath
 Flag.OPENCODE_DB = exerciseDatabasePath
 
 export const original = {
-  OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
   OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
   OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
 }

+ 7 - 9
packages/opencode/test/server/httpapi-exercise/index.ts

@@ -1,10 +1,10 @@
 /**
- * End-to-end exerciser for the legacy Hono instance routes and the Effect HttpApi routes.
+ * End-to-end exerciser for the Effect HttpApi routes.
  *
- * The goal is not to be a normal unit test file. This is a route-coverage and parity
- * harness we can run while deleting Hono: every public route should eventually have a
- * small scenario that proves the Effect route decodes requests, uses the right instance
- * context, mutates storage when expected, and returns a compatible response shape.
+ * The goal is not to be a normal unit test file. This is a route-coverage harness:
+ * every public route should have a small scenario that proves the route decodes
+ * requests, uses the right instance context, mutates storage when expected, and
+ * returns the expected response shape.
  *
  * The script intentionally isolates `OPENCODE_DB` before importing modules that touch
  * storage. Scenarios may create/delete sessions and reset the database after each run,
@@ -15,8 +15,7 @@
  * - `.seeded(...)` creates typed per-scenario state using Effect helpers on `ctx`.
  * - `.at(...)` builds the request from that typed state.
  * - `.json(...)` / `.jsonEffect(...)` assert response shape and optional side effects.
- * - `.mutating()` tells parity mode to run Effect and Hono in separate isolated contexts
- *   so destructive routes compare equivalent fresh setups instead of sharing one DB.
+ * - `.mutating()` tells the runner to reset isolated state after destructive routes.
  */
 import { Effect } from "effect"
 import { OpenApi } from "effect/unstable/httpapi"
@@ -1263,7 +1262,6 @@ const main = Effect.gen(function* () {
   const options = parseOptions(Bun.argv.slice(2))
   const modules = yield* Effect.promise(() => runtime())
   const effectRoutes = routeKeys(OpenApi.fromApi(modules.PublicApi))
-  const honoRoutes = routeKeys(yield* Effect.promise(() => modules.Server.openapiHono()))
   const selected = selectedScenarios(options, scenarios)
   const missing = effectRoutes.filter((route) => !scenarios.some((scenario) => route === routeKey(scenario)))
   const extra = scenarios.filter((scenario) => !effectRoutes.includes(routeKey(scenario)))
@@ -1274,7 +1272,7 @@ const main = Effect.gen(function* () {
     }
   }
 
-  printHeader(options, effectRoutes, honoRoutes, selected, missing, extra, {
+  printHeader(options, effectRoutes, selected, missing, extra, {
     database: exerciseDatabasePath,
     global: exerciseGlobalRoot,
   })

+ 1 - 2
packages/opencode/test/server/httpapi-exercise/report.ts

@@ -14,7 +14,6 @@ export const color = {
 export function printHeader(
   options: Options,
   effectRoutes: string[],
-  honoRoutes: string[],
   selected: Scenario[],
   missing: string[],
   extra: Scenario[],
@@ -24,7 +23,7 @@ export function printHeader(
   console.log(`${color.dim}db=${paths.database}${color.reset}`)
   console.log(`${color.dim}global=${paths.global}${color.reset}`)
   console.log(
-    `${color.dim}mode=${options.mode} selected=${selected.length} scenarioTimeout=${Duration.format(options.scenarioTimeout)} effectRoutes=${effectRoutes.length} missing=${missing.length} extra=${extra.length} onlyEffect=${effectRoutes.filter((route) => !honoRoutes.includes(route)).length} onlyHono=${honoRoutes.filter((route) => !effectRoutes.includes(route)).length}${color.reset}`,
+    `${color.dim}mode=${options.mode} selected=${selected.length} scenarioTimeout=${Duration.format(options.scenarioTimeout)} effectRoutes=${effectRoutes.length} missing=${missing.length} extra=${extra.length}${color.reset}`,
   )
   console.log("")
 }

+ 1 - 1
packages/opencode/test/server/httpapi-exercise/routing.ts

@@ -41,7 +41,7 @@ export function coverageResult(scenario: Scenario): Result {
 
 export function parseOptions(args: string[]): Options {
   const mode = option(args, "--mode") ?? "effect"
-  if (mode !== "effect" && mode !== "parity" && mode !== "coverage" && mode !== "auth")
+  if (mode !== "effect" && mode !== "coverage" && mode !== "auth")
     throw new Error(`invalid --mode ${mode}`)
   return {
     mode,

+ 0 - 54
packages/opencode/test/server/httpapi-exercise/runner.ts

@@ -5,13 +5,11 @@ import type { Config } from "../../../src/config/config"
 import { ModelID, ProviderID } from "../../../src/provider/schema"
 import type { MessageV2 } from "../../../src/session/message-v2"
 import { MessageID, PartID } from "../../../src/session/schema"
-import { stable } from "./assertions"
 import { call, callAuthProbe } from "./backend"
 import { original } from "./environment"
 import { runtime } from "./runtime"
 import type {
   ActiveScenario,
-  CallResult,
   Options,
   ProjectOptions,
   Result,
@@ -38,16 +36,6 @@ export function runScenario(options: Options) {
 function runActive(options: Options, scenario: ActiveScenario) {
   if (options.mode === "auth") return runAuth(scenario)
 
-  if (options.mode === "parity" && scenario.mutates && scenario.compare !== "none") {
-    return Effect.gen(function* () {
-      const effect = yield* runBackend(options, "effect", scenario)
-      const legacy = yield* runBackend(options, "legacy", scenario)
-      yield* trace(options, scenario, "compare start")
-      yield* compare(scenario, effect, legacy)
-      yield* trace(options, scenario, "compare done")
-    })
-  }
-
   return withContext(options, scenario, "shared", (ctx) =>
     Effect.gen(function* () {
       yield* trace(options, scenario, "effect request start")
@@ -56,17 +44,6 @@ function runActive(options: Options, scenario: ActiveScenario) {
       yield* trace(options, scenario, "effect expect start")
       yield* scenario.expect(ctx, ctx.state, effect)
       yield* trace(options, scenario, "effect expect done")
-      if (options.mode === "parity" && scenario.compare !== "none") {
-        yield* trace(options, scenario, "legacy request start")
-        const legacy = yield* call("legacy", scenario, ctx)
-        yield* trace(options, scenario, `legacy response ${legacy.status}`)
-        yield* trace(options, scenario, "legacy expect start")
-        yield* scenario.expect(ctx, ctx.state, legacy)
-        yield* trace(options, scenario, "legacy expect done")
-        yield* trace(options, scenario, "compare start")
-        yield* compare(scenario, effect, legacy)
-        yield* trace(options, scenario, "compare done")
-      }
     }),
   )
 }
@@ -74,38 +51,18 @@ function runActive(options: Options, scenario: ActiveScenario) {
 function runAuth(scenario: ActiveScenario) {
   return Effect.gen(function* () {
     const effect = yield* callAuthProbe("effect", scenario, "missing")
-    const legacy = yield* callAuthProbe("legacy", scenario, "missing")
     if (scenario.auth === "protected") {
       if (effect.status !== 401) throw new Error(`effect auth expected 401, got ${effect.status}`)
-      if (legacy.status !== 401) throw new Error(`legacy auth expected 401, got ${legacy.status}`)
       const effectAuthed = yield* callAuthProbe("effect", scenario, "valid")
-      const legacyAuthed = yield* callAuthProbe("legacy", scenario, "valid")
       if (effectAuthed.status === 401) throw new Error("effect auth rejected valid credentials")
-      if (legacyAuthed.status === 401) throw new Error("legacy auth rejected valid credentials")
       return
     }
 
     if (effect.status === 401) throw new Error("effect auth expected public access, got 401")
-    if (legacy.status === 401) throw new Error("legacy auth expected public access, got 401")
     if (effect.timedOut) throw new Error("effect auth expected public access, probe timed out")
-    if (legacy.timedOut) throw new Error("legacy auth expected public access, probe timed out")
   })
 }
 
-function runBackend(options: Options, backend: "effect" | "legacy", scenario: ActiveScenario) {
-  return withContext(options, scenario, backend, (ctx) =>
-    Effect.gen(function* () {
-      yield* trace(options, scenario, `${backend} request start`)
-      const result = yield* call(backend, scenario, ctx)
-      yield* trace(options, scenario, `${backend} response ${result.status}`)
-      yield* trace(options, scenario, `${backend} expect start`)
-      yield* scenario.expect(ctx, ctx.state, result)
-      yield* trace(options, scenario, `${backend} expect done`)
-      return result
-    }),
-  )
-}
-
 function withContext<A, E>(
   options: Options,
   scenario: ActiveScenario,
@@ -300,19 +257,8 @@ function fakeLlmConfig(url: string): Partial<Config.Info> {
   }
 }
 
-function compare(scenario: ActiveScenario, effect: CallResult, legacy: CallResult) {
-  return Effect.sync(() => {
-    if (effect.status !== legacy.status)
-      throw new Error(`legacy returned ${legacy.status}, effect returned ${effect.status}`)
-    if (scenario.compare === "status") return
-    if (stable(effect.body) !== stable(legacy.body))
-      throw new Error(`JSON parity mismatch\nlegacy: ${stable(legacy.body)}\neffect: ${stable(effect.body)}`)
-  })
-}
-
 const resetState = Effect.promise(async () => {
   const modules = await runtime()
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
   Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
   Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
   await modules.disposeAllInstances()

+ 0 - 3
packages/opencode/test/server/httpapi-exercise/runtime.ts

@@ -1,7 +1,6 @@
 export type Runtime = {
   PublicApi: (typeof import("../../../src/server/routes/instance/httpapi/public"))["PublicApi"]
   ExperimentalHttpApiServer: (typeof import("../../../src/server/routes/instance/httpapi/server"))["ExperimentalHttpApiServer"]
-  Server: (typeof import("../../../src/server/server"))["Server"]
   AppLayer: (typeof import("../../../src/effect/app-runtime"))["AppLayer"]
   InstanceRef: (typeof import("../../../src/effect/instance-ref"))["InstanceRef"]
   Instance: (typeof import("../../../src/project/instance"))["Instance"]
@@ -22,7 +21,6 @@ export function runtime() {
   return (runtimePromise ??= (async () => {
     const publicApi = await import("../../../src/server/routes/instance/httpapi/public")
     const httpApiServer = await import("../../../src/server/routes/instance/httpapi/server")
-    const server = await import("../../../src/server/server")
     const appRuntime = await import("../../../src/effect/app-runtime")
     const instanceRef = await import("../../../src/effect/instance-ref")
     const instance = await import("../../../src/project/instance")
@@ -37,7 +35,6 @@ export function runtime() {
     return {
       PublicApi: publicApi.PublicApi,
       ExperimentalHttpApiServer: httpApiServer.ExperimentalHttpApiServer,
-      Server: server.Server,
       AppLayer: appRuntime.AppLayer,
       InstanceRef: instanceRef.InstanceRef,
       Instance: instance.Instance,

+ 2 - 2
packages/opencode/test/server/httpapi-exercise/types.ts

@@ -10,8 +10,8 @@ export const Methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const
 
 export type Method = (typeof Methods)[number]
 export type OpenApiMethod = (typeof OpenApiMethods)[number]
-export type Mode = "effect" | "parity" | "coverage" | "auth"
-export type Backend = "effect" | "legacy"
+export type Mode = "effect" | "coverage" | "auth"
+export type Backend = "effect"
 export type Comparison = "none" | "status" | "json"
 export type CaptureMode = "full" | "stream"
 export type AuthPolicy = "protected" | "public" | "public-bypass" | "ticket-bypass"

+ 0 - 4
packages/opencode/test/server/httpapi-experimental.test.ts

@@ -1,6 +1,5 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import { Effect } from "effect"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { Instance } from "../../src/project/instance"
 import { WithInstance } from "../../src/project/with-instance"
 import { Server } from "../../src/server/server"
@@ -15,11 +14,9 @@ import { waitGlobalBusEventPromise } from "./global-bus"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 const testWorktreeMutations = process.platform === "win32" ? test.skip : test
 
 function app() {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
   return Server.Default().app
 }
 
@@ -39,7 +36,6 @@ async function waitReady(directory: string) {
 }
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   await disposeAllInstances()
   await resetDatabase()
 })

+ 0 - 122
packages/opencode/test/server/httpapi-instance.legacy.test.ts

@@ -1,122 +0,0 @@
-import { afterEach, describe, expect, test } from "bun:test"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import { Server } from "../../src/server/server"
-import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
-import * as Log from "@opencode-ai/core/util/log"
-import { resetDatabase } from "../fixture/db"
-import { disposeAllInstances, tmpdir } from "../fixture/fixture"
-import { waitGlobalBusEventPromise } from "./global-bus"
-
-void Log.init({ print: false })
-
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
-
-function app() {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
-  return Server.Default().app
-}
-
-async function waitDisposed(directory: string) {
-  await waitGlobalBusEventPromise({
-    message: "timed out waiting for instance disposal",
-    predicate: (event) => event.payload.type === "server.instance.disposed" && event.directory === directory,
-  })
-}
-
-afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
-  await disposeAllInstances()
-  await resetDatabase()
-})
-
-describe("instance HttpApi", () => {
-  test("serves catalog read endpoints through Hono bridge", async () => {
-    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
-
-    const [commands, agents, skills, lsp, formatter] = await Promise.all([
-      app().request(InstancePaths.command, { headers: { "x-opencode-directory": tmp.path } }),
-      app().request(InstancePaths.agent, { headers: { "x-opencode-directory": tmp.path } }),
-      app().request(InstancePaths.skill, { headers: { "x-opencode-directory": tmp.path } }),
-      app().request(InstancePaths.lsp, { headers: { "x-opencode-directory": tmp.path } }),
-      app().request(InstancePaths.formatter, { headers: { "x-opencode-directory": tmp.path } }),
-    ])
-
-    expect(commands.status).toBe(200)
-    expect(await commands.json()).toContainEqual(expect.objectContaining({ name: "init", source: "command" }))
-
-    expect(agents.status).toBe(200)
-    expect(await agents.json()).toContainEqual(expect.objectContaining({ name: "build", mode: "primary" }))
-
-    expect(skills.status).toBe(200)
-    expect(await skills.json()).toBeArray()
-
-    expect(lsp.status).toBe(200)
-    expect(await lsp.json()).toEqual([])
-
-    expect(formatter.status).toBe(200)
-    expect(await formatter.json()).toEqual([])
-  })
-
-  test("serves project git init through Hono bridge", async () => {
-    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
-    const disposed = waitDisposed(tmp.path)
-
-    const response = await app().request("/project/git/init", {
-      method: "POST",
-      headers: { "x-opencode-directory": tmp.path },
-    })
-
-    expect(response.status).toBe(200)
-    expect(await response.json()).toMatchObject({ vcs: "git", worktree: tmp.path })
-    await disposed
-
-    const current = await app().request("/project/current", { headers: { "x-opencode-directory": tmp.path } })
-    expect(current.status).toBe(200)
-    expect(await current.json()).toMatchObject({ vcs: "git", worktree: tmp.path })
-  })
-
-  test("serves project update through Hono bridge", async () => {
-    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
-
-    const current = await app().request("/project/current", { headers: { "x-opencode-directory": tmp.path } })
-    expect(current.status).toBe(200)
-    const project = (await current.json()) as { id: string }
-
-    const response = await app().request(`/project/${project.id}`, {
-      method: "PATCH",
-      headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
-      body: JSON.stringify({ name: "patched-project", commands: { start: "bun dev" } }),
-    })
-
-    expect(response.status).toBe(200)
-    expect(await response.json()).toMatchObject({
-      id: project.id,
-      name: "patched-project",
-      commands: { start: "bun dev" },
-    })
-
-    const list = await app().request("/project", { headers: { "x-opencode-directory": tmp.path } })
-    expect(list.status).toBe(200)
-    expect(await list.json()).toContainEqual(
-      expect.objectContaining({ id: project.id, name: "patched-project", commands: { start: "bun dev" } }),
-    )
-  })
-
-  test("serves instance dispose through Hono bridge", async () => {
-    await using tmp = await tmpdir()
-
-    const disposed = waitGlobalBusEventPromise({
-      message: "timed out waiting for instance disposal",
-      predicate: (event) => event.payload.type === "server.instance.disposed",
-    })
-
-    const response = await app().request(InstancePaths.dispose, {
-      method: "POST",
-      headers: { "x-opencode-directory": tmp.path },
-    })
-
-    expect(response.status).toBe(200)
-    expect(await response.json()).toBe(true)
-    expect((await disposed).directory).toBe(tmp.path)
-  })
-})

+ 5 - 9
packages/opencode/test/server/httpapi-instance.test.ts

@@ -14,22 +14,18 @@ import { resetDatabase } from "../fixture/db"
 import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
-// Flip the experimental HttpApi flag so backend selection telemetry on the
-// production routes reports the right backend, and the experimental
-// workspaces flag so SyncEvent.run actually writes to EventSequenceTable
-// (the source of truth the fence middleware reads). Reset the database
-// around the test so per-instance state does not leak between runs.
-// resetDatabase() already calls disposeAllInstances(), so we don't repeat it.
+// Flip the experimental workspaces flag so SyncEvent.run actually writes to
+// EventSequenceTable (the source of truth the fence middleware reads). Reset
+// the database around the test so per-instance state does not leak between
+// runs. resetDatabase() already calls disposeAllInstances(), so we don't
+// repeat it.
 const testStateLayer = Layer.effectDiscard(
   Effect.gen(function* () {
-    const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
     const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
     yield* Effect.promise(() => resetDatabase())
     yield* Effect.addFinalizer(() =>
       Effect.promise(async () => {
-        Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
         Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
         await resetDatabase()
       }),

+ 0 - 254
packages/opencode/test/server/httpapi-json-parity.test.ts

@@ -1,254 +0,0 @@
-import { afterEach, describe, expect } from "bun:test"
-import { Effect } from "effect"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import { ModelID, ProviderID } from "../../src/provider/schema"
-import { Instance } from "../../src/project/instance"
-import { Server } from "../../src/server/server"
-import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/groups/experimental"
-import { FilePaths } from "../../src/server/routes/instance/httpapi/groups/file"
-import { GlobalPaths } from "../../src/server/routes/instance/httpapi/groups/global"
-import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
-import { McpPaths } from "../../src/server/routes/instance/httpapi/groups/mcp"
-import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
-import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
-import { MessageID, PartID } from "../../src/session/schema"
-import { Session } from "@/session/session"
-import * as Log from "@opencode-ai/core/util/log"
-import { resetDatabase } from "../fixture/db"
-import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
-import { it } from "../lib/effect"
-
-void Log.init({ print: false })
-
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
-
-function app(experimental: boolean) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
-}
-type TestApp = ReturnType<typeof app>
-
-function pathFor(path: string, params: Record<string, string>) {
-  return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), path)
-}
-
-const seedSessions = Effect.gen(function* () {
-  const svc = yield* Session.Service
-  const parent = yield* svc.create({ title: "parent" })
-  yield* svc.create({ title: "child", parentID: parent.id })
-  const message = yield* svc.updateMessage({
-    id: MessageID.ascending(),
-    role: "user",
-    sessionID: parent.id,
-    agent: "build",
-    model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
-    time: { created: Date.now() },
-  })
-  yield* svc.updatePart({
-    id: PartID.ascending(),
-    sessionID: parent.id,
-    messageID: message.id,
-    type: "text",
-    text: "hello",
-  })
-  return { parent, message }
-})
-
-function withTmp<A, E, R>(
-  options: Parameters<typeof tmpdir>[0],
-  fn: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>,
-) {
-  return Effect.acquireRelease(
-    Effect.promise(() => tmpdir(options)),
-    (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
-  ).pipe(Effect.flatMap((tmp) => fn(tmp).pipe(provideInstance(tmp.path))))
-}
-
-function readJson(label: string, serverApp: TestApp, path: string, headers: HeadersInit) {
-  return Effect.promise(async () => {
-    const response = await serverApp.request(path, { headers })
-    if (response.status !== 200) throw new Error(`${label} returned ${response.status}: ${await response.text()}`)
-    return await response.json()
-  })
-}
-
-function expectJsonParity(input: {
-  label: string
-  legacy: TestApp
-  httpapi: TestApp
-  path: string
-  headers: HeadersInit
-}) {
-  return Effect.gen(function* () {
-    const legacy = yield* readJson(input.label, input.legacy, input.path, input.headers)
-    const httpapi = yield* readJson(input.label, input.httpapi, input.path, input.headers)
-    expect({ label: input.label, body: httpapi }).toEqual({ label: input.label, body: legacy })
-    return httpapi
-  })
-}
-
-afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
-  await disposeAllInstances()
-  await resetDatabase()
-})
-
-describe("HttpApi JSON parity", () => {
-  it.live(
-    "matches legacy JSON shape for safe GET endpoints",
-    withTmp(
-      {
-        git: true,
-        config: {
-          formatter: false,
-          lsp: false,
-          mcp: {
-            demo: {
-              type: "local",
-              command: ["echo", "demo"],
-              enabled: false,
-            },
-          },
-        },
-      },
-      (tmp) =>
-        Effect.gen(function* () {
-          yield* Effect.promise(() => Bun.write(`${tmp.path}/hello.txt`, "hello\n"))
-
-          const headers = { "x-opencode-directory": tmp.path }
-          const legacy = app(false)
-          const httpapi = app(true)
-
-          yield* Effect.forEach(
-            [
-              { label: "global.health", path: GlobalPaths.health, headers: {} },
-              { label: "global.config", path: GlobalPaths.config, headers: {} },
-              { label: "instance.path", path: InstancePaths.path, headers },
-              { label: "instance.vcs", path: InstancePaths.vcs, headers },
-              { label: "instance.vcsDiff", path: `${InstancePaths.vcsDiff}?mode=git`, headers },
-              { label: "instance.command", path: InstancePaths.command, headers },
-              { label: "instance.agent", path: InstancePaths.agent, headers },
-              { label: "instance.skill", path: InstancePaths.skill, headers },
-              { label: "instance.lsp", path: InstancePaths.lsp, headers },
-              { label: "instance.formatter", path: InstancePaths.formatter, headers },
-              { label: "config.get", path: "/config", headers },
-              { label: "config.providers", path: "/config/providers", headers },
-              { label: "project.list", path: "/project", headers },
-              { label: "project.current", path: "/project/current", headers },
-              { label: "provider.list", path: "/provider", headers },
-              { label: "provider.auth", path: "/provider/auth", headers },
-              { label: "permission.list", path: "/permission", headers },
-              { label: "question.list", path: "/question", headers },
-              { label: "mcp.status", path: McpPaths.status, headers },
-              { label: "pty.shells", path: PtyPaths.shells, headers },
-              { label: "pty.list", path: PtyPaths.list, headers },
-              { label: "file.list", path: `${FilePaths.list}?${new URLSearchParams({ path: "." })}`, headers },
-              {
-                label: "file.content",
-                path: `${FilePaths.content}?${new URLSearchParams({ path: "hello.txt" })}`,
-                headers,
-              },
-              { label: "file.status", path: FilePaths.status, headers },
-              {
-                label: "find.file",
-                path: `${FilePaths.findFile}?${new URLSearchParams({ query: "hello", dirs: "false" })}`,
-                headers,
-              },
-              {
-                label: "find.text",
-                path: `${FilePaths.findText}?${new URLSearchParams({ pattern: "hello" })}`,
-                headers,
-              },
-              {
-                label: "find.symbol",
-                path: `${FilePaths.findSymbol}?${new URLSearchParams({ query: "hello" })}`,
-                headers,
-              },
-              { label: "experimental.console", path: ExperimentalPaths.console, headers },
-              { label: "experimental.consoleOrgs", path: ExperimentalPaths.consoleOrgs, headers },
-              { label: "experimental.toolIDs", path: ExperimentalPaths.toolIDs, headers },
-              { label: "experimental.worktree", path: ExperimentalPaths.worktree, headers },
-              { label: "experimental.resource", path: ExperimentalPaths.resource, headers },
-            ],
-            (input) => expectJsonParity({ ...input, legacy, httpapi }),
-            { concurrency: 1 },
-          )
-        }),
-    ),
-  )
-
-  it.live(
-    "matches legacy JSON shape for session read endpoints",
-    withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
-      Effect.gen(function* () {
-        const headers = { "x-opencode-directory": tmp.path }
-        const seeded = yield* seedSessions.pipe(Effect.provide(Session.defaultLayer))
-        const legacy = app(false)
-        const httpapi = app(true)
-
-        const rootsFalse = yield* expectJsonParity({
-          label: "session.list roots false",
-          legacy,
-          httpapi,
-          path: `${SessionPaths.list}?roots=false`,
-          headers,
-        })
-        expect((rootsFalse as Session.Info[]).map((session) => session.id)).toContain(seeded.parent.id)
-        expect((rootsFalse as Session.Info[]).length).toBe(2)
-
-        const experimentalRootsFalse = yield* expectJsonParity({
-          label: "experimental.session roots false",
-          legacy,
-          httpapi,
-          path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10", roots: "false" })}`,
-          headers,
-        })
-        expect((experimentalRootsFalse as Session.GlobalInfo[]).length).toBe(2)
-
-        const experimentalArchivedFalse = yield* expectJsonParity({
-          label: "experimental.session archived false",
-          legacy,
-          httpapi,
-          path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10", archived: "false" })}`,
-          headers,
-        })
-        expect((experimentalArchivedFalse as Session.GlobalInfo[]).length).toBe(2)
-
-        yield* Effect.forEach(
-          [
-            { label: "session.list roots", path: `${SessionPaths.list}?roots=true`, headers },
-            { label: "session.list all", path: SessionPaths.list, headers },
-            { label: "session.get", path: pathFor(SessionPaths.get, { sessionID: seeded.parent.id }), headers },
-            {
-              label: "session.children",
-              path: pathFor(SessionPaths.children, { sessionID: seeded.parent.id }),
-              headers,
-            },
-            {
-              label: "session.messages",
-              path: pathFor(SessionPaths.messages, { sessionID: seeded.parent.id }),
-              headers,
-            },
-            {
-              label: "session.messages empty before",
-              path: `${pathFor(SessionPaths.messages, { sessionID: seeded.parent.id })}?before=`,
-              headers,
-            },
-            {
-              label: "session.message",
-              path: pathFor(SessionPaths.message, { sessionID: seeded.parent.id, messageID: seeded.message.id }),
-              headers,
-            },
-            {
-              label: "experimental.session",
-              path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10" })}`,
-              headers,
-            },
-          ],
-          (input) => expectJsonParity({ ...input, legacy, httpapi }),
-          { concurrency: 1 },
-        )
-      }),
-    ),
-  )
-})

+ 16 - 38
packages/opencode/test/server/httpapi-listen.test.ts

@@ -10,7 +10,6 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
 void Log.init({ print: false })
 
 const original = {
-  OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
   OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
   OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
   envPassword: process.env.OPENCODE_SERVER_PASSWORD,
@@ -20,7 +19,6 @@ const auth = { username: "opencode", password: "listen-secret" }
 const testPty = process.platform === "win32" ? test.skip : test
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
   Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
   Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
   if (original.envPassword === undefined) delete process.env.OPENCODE_SERVER_PASSWORD
@@ -31,8 +29,7 @@ afterEach(async () => {
   await resetDatabase()
 })
 
-async function startListener(backend: "effect-httpapi" | "hono" = "effect-httpapi") {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "effect-httpapi"
+async function startListener() {
   Flag.OPENCODE_SERVER_PASSWORD = auth.password
   Flag.OPENCODE_SERVER_USERNAME = auth.username
   process.env.OPENCODE_SERVER_PASSWORD = auth.password
@@ -40,8 +37,7 @@ async function startListener(backend: "effect-httpapi" | "hono" = "effect-httpap
   return Server.listen({ hostname: "127.0.0.1", port: 0 })
 }
 
-async function startNoAuthListener(backend: "effect-httpapi" | "hono" = "effect-httpapi") {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "effect-httpapi"
+async function startNoAuthListener() {
   Flag.OPENCODE_SERVER_PASSWORD = undefined
   Flag.OPENCODE_SERVER_USERNAME = auth.username
   delete process.env.OPENCODE_SERVER_PASSWORD
@@ -212,22 +208,6 @@ describe("HttpApi Server.listen", () => {
     }
   })
 
-  testPty("serves PTY websocket tickets through legacy Hono Server.listen", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
-    const listener = await startListener("hono")
-    try {
-      const info = await createCat(listener, tmp.path)
-      const ticket = await connectTicket(listener, info.id, tmp.path)
-      const ws = await openSocket(socketURL(listener, info.id, tmp.path, ticket.ticket))
-      const message = waitForMessage(ws, (message) => message.includes("ping-hono-ticket"))
-      ws.send("ping-hono-ticket\n")
-      expect(await message).toContain("ping-hono-ticket")
-      ws.close(1000)
-    } finally {
-      await stop(listener, "timed out cleaning up hono listener").catch(() => undefined)
-    }
-  })
-
   testPty("rejects unsafe PTY ticket mint and connect requests", async () => {
     await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
     const listener = await startListener()
@@ -300,20 +280,18 @@ describe("HttpApi Server.listen", () => {
     }
   })
 
-  for (const backend of ["effect-httpapi", "hono"] as const) {
-    testPty(`keeps PTY websocket tickets optional when server auth is disabled (${backend})`, async () => {
-      await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
-      const listener = await startNoAuthListener(backend)
-      try {
-        const info = await createCat(listener, tmp.path)
-        const ws = await openSocket(socketURL(listener, info.id, tmp.path))
-        const message = waitForMessage(ws, (message) => message.includes(`ping-no-auth-${backend}`))
-        ws.send(`ping-no-auth-${backend}\n`)
-        expect(await message).toContain(`ping-no-auth-${backend}`)
-        ws.close(1000)
-      } finally {
-        await stop(listener, "timed out cleaning up no-auth listener").catch(() => undefined)
-      }
-    })
-  }
+  testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => {
+    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
+    const listener = await startNoAuthListener()
+    try {
+      const info = await createCat(listener, tmp.path)
+      const ws = await openSocket(socketURL(listener, info.id, tmp.path))
+      const message = waitForMessage(ws, (message) => message.includes("ping-no-auth"))
+      ws.send("ping-no-auth\n")
+      expect(await message).toContain("ping-no-auth")
+      ws.close(1000)
+    } finally {
+      await stop(listener, "timed out cleaning up no-auth listener").catch(() => undefined)
+    }
+  })
 })

+ 5 - 13
packages/opencode/test/server/httpapi-mcp.test.ts

@@ -1,7 +1,6 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import { Context, Effect, FileSystem, Layer, Path } from "effect"
 import { NodeFileSystem, NodePath } from "@effect/platform-node"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
 import { McpPaths } from "../../src/server/routes/instance/httpapi/groups/mcp"
 import { Instance } from "../../src/project/instance"
@@ -15,13 +14,11 @@ import { testEffect } from "../lib/effect"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 const context = Context.empty() as Context.Context<unknown>
 const it = testEffect(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
 
-function app(experimental: boolean) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
+function app() {
+  return Server.Default().app
 }
 type TestApp = ReturnType<typeof app>
 
@@ -79,7 +76,6 @@ const readResponse = Effect.fnUntraced(function* (input: { app: TestApp; path: s
 })
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   await disposeAllInstances()
   await resetDatabase()
 })
@@ -165,23 +161,19 @@ describe("mcp HttpApi", () => {
   })
 
   it.live(
-    "matches legacy unsupported OAuth error responses",
+    "returns unsupported OAuth error responses",
     withMcpProject((dir) =>
       Effect.gen(function* () {
         const headers = { "x-opencode-directory": dir }
-        const legacy = app(false)
-        const httpapi = app(true)
 
         yield* Effect.forEach(["/mcp/demo/auth", "/mcp/demo/auth/authenticate"], (path) =>
           Effect.gen(function* () {
-            const legacyResponse = yield* readResponse({ app: legacy, path, headers })
-            const httpapiResponse = yield* readResponse({ app: httpapi, path, headers })
+            const response = yield* readResponse({ app: app(), path, headers })
 
-            expect(legacyResponse).toEqual({
+            expect(response).toEqual({
               status: 400,
               body: JSON.stringify({ error: "MCP server demo does not support OAuth" }),
             })
-            expect(httpapiResponse).toEqual(legacyResponse)
           }),
         )
       }),

+ 0 - 127
packages/opencode/test/server/httpapi-parity.test.ts

@@ -1,127 +0,0 @@
-import { afterEach, describe, expect, test } from "bun:test"
-import { Effect } from "effect"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import * as Log from "@opencode-ai/core/util/log"
-import { WithInstance } from "../../src/project/with-instance"
-import { Server } from "../../src/server/server"
-import { Session } from "@/session/session"
-import { MessageID } from "../../src/session/schema"
-import { ModelID, ProviderID } from "../../src/provider/schema"
-import { resetDatabase } from "../fixture/db"
-import { disposeAllInstances, tmpdir } from "../fixture/fixture"
-
-void Log.init({ print: false })
-
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
-
-afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
-  await disposeAllInstances()
-  await resetDatabase()
-})
-
-function app(experimental: boolean) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
-}
-
-function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
-  return Effect.runPromise(fx.pipe(Effect.provide(Session.defaultLayer)))
-}
-
-function createSessionWithMessages(directory: string, count: number) {
-  return WithInstance.provide({
-    directory,
-    fn: async () => {
-      const session = await runSession(Session.Service.use((svc) => svc.create({})))
-      for (let i = 0; i < count; i++) {
-        await runSession(
-          Effect.gen(function* () {
-            const svc = yield* Session.Service
-            yield* svc.updateMessage({
-              id: MessageID.ascending(),
-              role: "user",
-              sessionID: session.id,
-              agent: "build",
-              model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
-              time: { created: Date.now() },
-            })
-          }),
-        )
-      }
-      return session.id
-    },
-  })
-}
-
-// ──────────────────────────────────────────────────────────────────────────────
-// Reproducer 1: Link header should reflect the request's actual Host header,
-// not "localhost". HttpApi uses `new URL(request.url, "http://localhost")`
-// which embeds localhost because request.url is path-only. Fix: use
-// `HttpServerRequest.toURL(request)` which honors the Host header.
-// ──────────────────────────────────────────────────────────────────────────────
-describe("Link header host", () => {
-  test("HttpApi pagination Link header echoes request host", async () => {
-    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
-    const sessionID = await createSessionWithMessages(tmp.path, 3)
-
-    const response = await app(true).request(`/session/${sessionID}/message?limit=2`, {
-      headers: {
-        host: "opencode.test:4096",
-        "x-opencode-directory": tmp.path,
-      },
-    })
-
-    expect(response.status).toBe(200)
-    const link = response.headers.get("link")
-    expect(link).not.toBeNull()
-    // Link should contain the request's Host, not "localhost".
-    expect(link).toContain("opencode.test")
-    expect(link).not.toContain("localhost")
-  })
-})
-
-// ──────────────────────────────────────────────────────────────────────────────
-// Reproducer 2: GET /session/{missing-id}/todo should return 404, not 500.
-// The session.todo handler in HttpApi doesn't wrap with `mapNotFound`, so a
-// `NotFoundError` from the service surfaces as a defect → 500. Hono's
-// equivalent maps to 404 via `errors.notFound`.
-//
-// Affected endpoints (handlers without mapNotFound): todo, diff, summarize,
-// fork, abort, init, deleteMessage, command, shell, revert, unrevert.
-//
-// FIXME: unskip when mapNotFound coverage is added (next PR).
-// ──────────────────────────────────────────────────────────────────────────────
-describe("404 mapping for missing session", () => {
-  test.todo("HttpApi /session/{missing}/todo returns 404 not 500", async () => {
-    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
-
-    const response = await app(true).request("/session/ses_does_not_exist/todo", {
-      headers: { "x-opencode-directory": tmp.path },
-    })
-
-    expect(response.status).toBe(404)
-  })
-})
-
-// ──────────────────────────────────────────────────────────────────────────────
-// Reproducer 3: 404 response body shape should match Hono's public NamedError
-// envelope `{ name, data: { message } }`. SDK consumers read
-// `error.data.message`, so returning an Effect built-in `{ _tag }` body is a
-// compatibility break.
-// ──────────────────────────────────────────────────────────────────────────────
-describe("Error JSON shape parity", () => {
-  test("HttpApi 404 body matches Hono shape", async () => {
-    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
-    const headers = { "x-opencode-directory": tmp.path }
-
-    const hono = await app(false).request("/session/ses_does_not_exist", { headers })
-    const httpapi = await app(true).request("/session/ses_does_not_exist", { headers })
-
-    expect(httpapi.status).toBe(hono.status)
-    const body = (await httpapi.json()) as { name?: string; data?: { message?: string } }
-    expect(body).toEqual(await hono.json())
-    expect(body.name).toBe("NotFoundError")
-    expect(typeof body.data?.message).toBe("string")
-  })
-})

+ 14 - 34
packages/opencode/test/server/httpapi-provider.test.ts

@@ -1,7 +1,6 @@
 import { afterEach, describe, expect } from "bun:test"
 import { Effect, FileSystem, Layer, Path } from "effect"
 import { NodeFileSystem, NodePath } from "@effect/platform-node"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { Instance } from "../../src/project/instance"
 import { WithInstance } from "../../src/project/with-instance"
 import { InstanceRuntime } from "../../src/project/instance-runtime"
@@ -13,15 +12,13 @@ import { testEffect } from "../lib/effect"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 const it = testEffect(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
 const providerID = "test-oauth-parity"
 const oauthURL = "https://example.com/oauth"
 const oauthInstructions = "Finish OAuth"
 
-function app(experimental: boolean) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
+function app() {
+  return Server.Default().app
 }
 
 function requestAuthorize(input: {
@@ -101,54 +98,37 @@ function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E,
 }
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   await disposeAllInstances()
   await resetDatabase()
 })
 
 describe("provider HttpApi", () => {
   it.live(
-    "matches legacy OAuth authorize response shapes",
+    "serves OAuth authorize response shapes",
     withProviderProject((dir) =>
       Effect.gen(function* () {
         const headers = { "x-opencode-directory": dir, "content-type": "application/json" }
-        const legacy = app(false)
-        const httpapi = app(true)
+        const server = app()
 
-        const apiLegacy = yield* requestAuthorize({
-          app: legacy,
+        const api = yield* requestAuthorize({
+          app: server,
           providerID,
           method: 0,
           headers,
         })
-        const apiHttpApi = yield* requestAuthorize({
-          app: httpapi,
-          providerID,
-          method: 0,
-          headers,
-        })
-        expect(apiLegacy).toEqual({ status: 200, body: "" })
-        // #26474 changed the HTTP API authorize handler to serialize an
-        // undefined service result as JSON `null` instead of an empty body
-        // so clients can `.json()` parse the response uniformly. The legacy
-        // Hono path still emits an empty body (`c.json(undefined)`); the new
-        // backend's body diverges intentionally.
-        expect(apiHttpApi).toEqual({ status: 200, body: "null" })
+        // method 0 (api-key style) — authorize() resolves with no further
+        // redirect; #26474 changed the wire format to JSON `null` so clients
+        // can `.json()` parse uniformly instead of getting an empty body
+        // that throws.
+        expect(api).toEqual({ status: 200, body: "null" })
 
-        const oauthLegacy = yield* requestAuthorize({
-          app: legacy,
-          providerID,
-          method: 1,
-          headers,
-        })
-        const oauthHttpApi = yield* requestAuthorize({
-          app: httpapi,
+        const oauth = yield* requestAuthorize({
+          app: server,
           providerID,
           method: 1,
           headers,
         })
-        expect(oauthHttpApi).toEqual(oauthLegacy)
-        expect(JSON.parse(oauthHttpApi.body)).toEqual({
+        expect(JSON.parse(oauth.body)).toEqual({
           url: oauthURL,
           method: "code",
           instructions: oauthInstructions,

+ 2 - 20
packages/opencode/test/server/httpapi-pty.test.ts

@@ -1,6 +1,5 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import { NodeHttpServer, NodeServices } from "@effect/platform-node"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { PtyID } from "../../src/pty/schema"
 import { Instance } from "../../src/project/instance"
 import { Server } from "../../src/server/server"
@@ -17,16 +16,13 @@ import { testEffect } from "../lib/effect"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 const testPty = process.platform === "win32" ? test.skip : test
 
 const testStateLayer = Layer.effectDiscard(
   Effect.gen(function* () {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     yield* Effect.promise(() => resetDatabase())
     yield* Effect.addFinalizer(() =>
       Effect.promise(async () => {
-        Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
         await resetDatabase()
       }),
     )
@@ -50,9 +46,8 @@ const effectIt = testEffect(
   ),
 )
 
-function app(experimental = true) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
+function app() {
+  return Server.Default().app
 }
 
 function serverUrl() {
@@ -62,7 +57,6 @@ function serverUrl() {
 const directoryHeader = (dir: string) => HttpClientRequest.setHeader("x-opencode-directory", dir)
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   await disposeAllInstances()
   await resetDatabase()
 })
@@ -121,18 +115,6 @@ describe("pty HttpApi bridge", () => {
     expect(missing.status).toBe(404)
   })
 
-  test("matches Hono missing PTY error body", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
-    const headers = { "x-opencode-directory": tmp.path }
-    const path = PtyPaths.get.replace(":ptyID", PtyID.ascending())
-
-    const hono = await app(false).request(path, { headers })
-    const httpapi = await app().request(path, { headers })
-
-    expect(httpapi.status).toBe(hono.status)
-    expect(await httpapi.json()).toEqual(await hono.json())
-  })
-
   test("returns 404 for missing PTY websocket before upgrade", async () => {
     await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
     const response = await app().request(PtyPaths.connect.replace(":ptyID", PtyID.ascending()), {

+ 0 - 4
packages/opencode/test/server/httpapi-raw-route-auth.test.ts

@@ -1,7 +1,6 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import { ConfigProvider, Layer } from "effect"
 import { HttpRouter } from "effect/unstable/http"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { Instance } from "../../src/project/instance"
 import { EventPaths } from "../../src/server/routes/instance/httpapi/event"
 import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
@@ -13,10 +12,8 @@ import * as Log from "@opencode-ai/core/util/log"
 
 void Log.init({ print: false })
 
-const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 
 function app(input: { password?: string; username?: string }) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
   const handler = HttpRouter.toWebHandler(
     ExperimentalHttpApiServer.routes.pipe(
       Layer.provide(
@@ -48,7 +45,6 @@ async function cancelBody(response: Response) {
 }
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
   await disposeAllInstances()
   await resetDatabase()
 })

+ 53 - 55
packages/opencode/test/server/httpapi-sdk.test.ts

@@ -22,23 +22,21 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
 import { it } from "../lib/effect"
 
 const original = {
-  OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
   OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
   OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
 }
 
-type Backend = "legacy" | "httpapi"
+type ServerPath = "default" | "raw"
 type Sdk = ReturnType<typeof createOpencodeClient>
 type SdkResult = { response: Response; data?: unknown; error?: unknown }
 type Captured = { status: number; data?: unknown; error?: unknown }
 type ProjectFixture = { sdk: Sdk; directory: string }
 type LlmProjectFixture = ProjectFixture & { llm: TestLLMServer["Service"] }
 
-function app(backend: Backend, input?: { password?: string; username?: string }) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "httpapi"
+function app(serverPath: ServerPath, input?: { password?: string; username?: string }) {
   Flag.OPENCODE_SERVER_PASSWORD = input?.password
   Flag.OPENCODE_SERVER_USERNAME = input?.username
-  if (backend === "legacy") return Server.Legacy().app
+  if (serverPath === "default") return Server.Default().app
 
   const handler = HttpRouter.toWebHandler(
     ExperimentalHttpApiServer.routes.pipe(
@@ -62,7 +60,7 @@ function app(backend: Backend, input?: { password?: string; username?: string })
 }
 
 function client(
-  backend: Backend,
+  serverPath: ServerPath,
   directory?: string,
   input?: { password?: string; username?: string; headers?: Record<string, string> },
 ) {
@@ -70,12 +68,12 @@ function client(
     baseUrl: "http://localhost",
     directory,
     headers: input?.headers,
-    fetch: serverFetch(backend, input),
+    fetch: serverFetch(serverPath, input),
   })
 }
 
-function serverFetch(backend: Backend, input?: { password?: string; username?: string }) {
-  const serverApp = app(backend, input)
+function serverFetch(serverPath: ServerPath, input?: { password?: string; username?: string }) {
+  const serverApp = app(serverPath, input)
   return Object.assign(
     async (request: RequestInfo | URL, init?: RequestInit) =>
       await serverApp.fetch(request instanceof Request ? request : new Request(request, init)),
@@ -194,20 +192,20 @@ function httpapi<A, E>(name: string, effect: Effect.Effect<A, E, Scope.Scope>) {
   it.live(name, effect)
 }
 
-function parity<A, E>(name: string, scenario: (backend: Backend) => Effect.Effect<A, E, Scope.Scope>) {
+function serverPathParity<A, E>(name: string, scenario: (serverPath: ServerPath) => Effect.Effect<A, E, Scope.Scope>) {
   it.live(
     name,
     Effect.gen(function* () {
-      const legacy = yield* scenario("legacy")
+      const standard = yield* scenario("default")
       yield* resetState()
-      const httpapi = yield* scenario("httpapi")
-      expect(httpapi).toEqual(legacy)
+      const raw = yield* scenario("raw")
+      expect(raw).toEqual(standard)
     }),
   )
 }
 
 function withProject<A, E, R>(
-  backend: Backend,
+  serverPath: ServerPath,
   options: { git?: boolean; config?: Partial<Config.Info>; setup?: (dir: string) => Effect.Effect<void> },
   run: (input: ProjectFixture) => Effect.Effect<A, E, R>,
 ) {
@@ -216,30 +214,30 @@ function withProject<A, E, R>(
     (tmp) => call(() => tmp[Symbol.asyncDispose]()).pipe(Effect.ignore),
   ).pipe(
     Effect.tap((tmp) => options.setup?.(tmp.path) ?? Effect.void),
-    Effect.flatMap((tmp) => run({ sdk: client(backend, tmp.path), directory: tmp.path })),
+    Effect.flatMap((tmp) => run({ sdk: client(serverPath, tmp.path), directory: tmp.path })),
   )
 }
 
-function withStandardProject<A, E, R>(backend: Backend, run: (input: ProjectFixture) => Effect.Effect<A, E, R>) {
-  return withProject(backend, { setup: writeStandardFiles }, run)
+function withStandardProject<A, E, R>(serverPath: ServerPath, run: (input: ProjectFixture) => Effect.Effect<A, E, R>) {
+  return withProject(serverPath, { setup: writeStandardFiles }, run)
 }
 
-function withFakeLlm<A, E, R>(backend: Backend, run: (input: LlmProjectFixture) => Effect.Effect<A, E, R>) {
+function withFakeLlm<A, E, R>(serverPath: ServerPath, run: (input: LlmProjectFixture) => Effect.Effect<A, E, R>) {
   return Effect.gen(function* () {
     const llm = yield* TestLLMServer
-    return yield* withProject(backend, { config: providerConfig(llm.url) }, (input) => run({ ...input, llm }))
+    return yield* withProject(serverPath, { config: providerConfig(llm.url) }, (input) => run({ ...input, llm }))
   }).pipe(Effect.provide(TestLLMServer.layer))
 }
 
 function withFakeLlmProject<A, E, R>(
-  backend: Backend,
+  serverPath: ServerPath,
   options: { setup?: (dir: string) => Effect.Effect<void> },
   run: (input: LlmProjectFixture) => Effect.Effect<A, E, R>,
 ) {
   return Effect.gen(function* () {
     const llm = yield* TestLLMServer
     return yield* withProject(
-      backend,
+      serverPath,
       {
         config: providerConfig(llm.url),
         setup: options.setup,
@@ -306,7 +304,6 @@ function seedMessage(directory: string, sessionID: string) {
 }
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
   Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
   Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
   await disposeAllInstances()
@@ -317,7 +314,7 @@ describe("HttpApi SDK", () => {
   httpapi(
     "uses the generated SDK for global and control routes",
     Effect.gen(function* () {
-      const sdk = client("httpapi")
+      const sdk = client("raw")
       const health = yield* call(() => sdk.global.health())
       const log = yield* call(() => sdk.app.log({ service: "httpapi-sdk-test", level: "info", message: "hello" }))
 
@@ -334,7 +331,7 @@ describe("HttpApi SDK", () => {
 
   httpapi(
     "uses the generated SDK for safe instance routes",
-    withProject("httpapi", { git: false, setup: writeStandardFiles }, ({ sdk }) =>
+    withProject("raw", { git: false, setup: writeStandardFiles }, ({ sdk }) =>
       Effect.gen(function* () {
         const file = yield* call(() => sdk.file.read({ path: "hello.txt" }))
         const session = yield* call(() => sdk.session.create({ title: "sdk" }))
@@ -357,9 +354,9 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK global and control behavior across backends", (backend) =>
+  serverPathParity("matches generated SDK global and control behavior", (serverPath) =>
     Effect.gen(function* () {
-      const sdk = client(backend)
+      const sdk = client(serverPath)
       const health = yield* capture(() => sdk.global.health())
       const log = yield* capture(() => sdk.app.log({ service: "sdk-parity", level: "info", message: "hello" }))
       const invalidAuth = yield* capture(() => sdk.auth.set({ providerID: "test" }))
@@ -372,22 +369,22 @@ describe("HttpApi SDK", () => {
     }),
   )
 
-  parity("matches generated SDK global event stream across backends", (backend) =>
-    firstEvent(() => client(backend).global.event({ signal: AbortSignal.timeout(1_000) })).pipe(
+  serverPathParity("matches generated SDK global event stream", (serverPath) =>
+    firstEvent(() => client(serverPath).global.event({ signal: AbortSignal.timeout(1_000) })).pipe(
       Effect.map((event) => ({ type: record(record(event).payload).type })),
     ),
   )
 
-  parity("matches generated SDK instance event stream across backends", (backend) =>
-    withStandardProject(backend, ({ sdk }) =>
+  serverPathParity("matches generated SDK instance event stream", (serverPath) =>
+    withStandardProject(serverPath, ({ sdk }) =>
       firstEvent(() => sdk.event.subscribe(undefined, { signal: AbortSignal.timeout(1_000) })).pipe(
         Effect.map((event) => ({ type: record(record(event).payload).type })),
       ),
     ),
   )
 
-  parity("matches generated SDK missing session errors across backends", (backend) =>
-    withStandardProject(backend, ({ sdk }) =>
+  serverPathParity("matches generated SDK missing session errors", (serverPath) =>
+    withStandardProject(serverPath, ({ sdk }) =>
       Effect.gen(function* () {
         const sessionID = "ses_missing"
         const expected = {
@@ -408,8 +405,8 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("formats missing session validation errors for -s", (backend) =>
-    withStandardProject(backend, ({ directory }) =>
+  serverPathParity("formats missing session validation errors for -s", (serverPath) =>
+    withStandardProject(serverPath, ({ directory }) =>
       Effect.gen(function* () {
         const sessionID = "ses_206f84f18ffeZ6hhD7pFYAiW5T"
         const thrown = yield* captureThrown(() =>
@@ -417,7 +414,7 @@ describe("HttpApi SDK", () => {
             url: "http://localhost",
             directory,
             sessionID,
-            fetch: serverFetch(backend),
+            fetch: serverFetch(serverPath),
           }),
         )
         expect(errorMessage(thrown)).toBe(`Session not found: ${sessionID}`)
@@ -426,20 +423,21 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK basic auth behavior across backends", (backend) =>
-    withStandardProject(backend, ({ directory }) =>
+  httpapi(
+    "uses generated SDK basic auth behavior",
+    withStandardProject("raw", ({ directory }) =>
       Effect.gen(function* () {
         const missing = yield* capture(() =>
-          client(backend, directory, { password: "secret" }).file.read({ path: "hello.txt" }),
+          client("raw", directory, { password: "secret" }).file.read({ path: "hello.txt" }),
         )
         const bad = yield* capture(() =>
-          client(backend, directory, {
+          client("raw", directory, {
             password: "secret",
             headers: { authorization: authorization("opencode", "wrong") },
           }).file.read({ path: "hello.txt" }),
         )
         const good = yield* capture(() =>
-          client(backend, directory, {
+          client("raw", directory, {
             password: "secret",
             headers: { authorization: authorization("opencode", "secret") },
           }).file.read({ path: "hello.txt" }),
@@ -453,8 +451,8 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK instance read routes across backends", (backend) =>
-    withStandardProject(backend, ({ sdk, directory }) =>
+  serverPathParity("matches generated SDK instance read routes", (serverPath) =>
+    withStandardProject(serverPath, ({ sdk, directory }) =>
       Effect.gen(function* () {
         const project = yield* capture(() => sdk.project.current())
         const projects = yield* capture(() => sdk.project.list())
@@ -504,8 +502,8 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK session lifecycle routes across backends", (backend) =>
-    withStandardProject(backend, ({ sdk }) =>
+  serverPathParity("matches generated SDK session lifecycle routes", (serverPath) =>
+    withStandardProject(serverPath, ({ sdk }) =>
       Effect.gen(function* () {
         const parent = yield* capture(() => sdk.session.create({ title: "parent" }))
         const parentID = String(record(parent.data).id)
@@ -557,8 +555,8 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK session message and part routes across backends", (backend) =>
-    withStandardProject(backend, ({ sdk, directory }) =>
+  serverPathParity("matches generated SDK session message and part routes", (serverPath) =>
+    withStandardProject(serverPath, ({ sdk, directory }) =>
       Effect.gen(function* () {
         const session = yield* capture(() => sdk.session.create({ title: "messages" }))
         const sessionID = String(record(session.data).id)
@@ -609,8 +607,8 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK prompt no-reply routes across backends", (backend) =>
-    withStandardProject(backend, ({ sdk }) =>
+  serverPathParity("matches generated SDK prompt no-reply routes", (serverPath) =>
+    withStandardProject(serverPath, ({ sdk }) =>
       Effect.gen(function* () {
         const session = yield* capture(() => sdk.session.create({ title: "prompt" }))
         const sessionID = String(record(session.data).id)
@@ -646,8 +644,8 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK prompt streaming through fake LLM across backends", (backend) =>
-    withFakeLlm(backend, ({ sdk, llm }) =>
+  serverPathParity("matches generated SDK prompt streaming through fake LLM", (serverPath) =>
+    withFakeLlm(serverPath, ({ sdk, llm }) =>
       Effect.gen(function* () {
         yield* llm.text("fake world", { usage: { input: 11, output: 7 } })
         const session = yield* capture(() =>
@@ -682,7 +680,7 @@ describe("HttpApi SDK", () => {
 
   httpapi(
     "includes project skills in REST API async prompt context",
-    withFakeLlmProject("httpapi", { setup: writeProjectSkill }, ({ sdk, llm }) =>
+    withFakeLlmProject("default", { setup: writeProjectSkill }, ({ sdk, llm }) =>
       Effect.gen(function* () {
         yield* llm.text("skill context ok", { usage: { input: 11, output: 7 } })
         const session = yield* capture(() =>
@@ -710,8 +708,8 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK TUI validation and command routes across backends", (backend) =>
-    withStandardProject(backend, ({ sdk }) =>
+  serverPathParity("matches generated SDK TUI validation and command routes", (serverPath) =>
+    withStandardProject(serverPath, ({ sdk }) =>
       Effect.gen(function* () {
         const session = yield* capture(() => sdk.session.create({ title: "tui" }))
         const sessionID = String(record(session.data).id)
@@ -761,8 +759,8 @@ describe("HttpApi SDK", () => {
     ),
   )
 
-  parity("matches generated SDK project git initialization across backends", (backend) =>
-    withProject(backend, { git: false }, ({ sdk, directory }) =>
+  serverPathParity("matches generated SDK project git initialization", (serverPath) =>
+    withProject(serverPath, { git: false }, ({ sdk, directory }) =>
       Effect.gen(function* () {
         const before = yield* capture(() => sdk.project.current())
         const init = yield* capture(() => sdk.project.initGit())

+ 23 - 50
packages/opencode/test/server/httpapi-session.test.ts

@@ -30,16 +30,14 @@ import { it } from "../lib/effect"
 
 void Log.init({ print: false })
 
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
 const workspaceLayer = Workspace.defaultLayer.pipe(
   Layer.provide(InstanceStore.defaultLayer),
   Layer.provide(InstanceBootstrap.defaultLayer),
 )
 
-function app(experimental = true) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
+function app() {
+  return Server.Default().app
 }
 
 function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
@@ -119,10 +117,6 @@ function request(path: string, init?: RequestInit) {
   return Effect.promise(async () => app().request(path, init))
 }
 
-function requestWithBackend(experimental: boolean, path: string, init?: RequestInit) {
-  return Effect.promise(async () => app(experimental).request(path, init))
-}
-
 function json<T>(response: Response) {
   return Effect.promise(async () => {
     if (response.status !== 200) throw new Error(await response.text())
@@ -149,7 +143,6 @@ function withTmp<A, E, R>(
 }
 
 afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
   Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
   await disposeAllInstances()
   await resetDatabase()
@@ -198,7 +191,7 @@ describe("session HttpApi", () => {
   )
 
   it.live(
-    "serves read routes through Hono bridge",
+    "serves read routes",
     withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
       Effect.gen(function* () {
         const headers = { "x-opencode-directory": tmp.path }
@@ -305,7 +298,7 @@ describe("session HttpApi", () => {
   )
 
   it.live(
-    "serves lifecycle mutation routes through Hono bridge",
+    "serves lifecycle mutation routes",
     withTmp({ git: true, config: { formatter: false, lsp: false, share: "disabled" } }, (tmp) =>
       Effect.gen(function* () {
         const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
@@ -389,39 +382,26 @@ describe("session HttpApi", () => {
   )
 
   it.live(
-    "matches legacy archived timestamp validation",
+    "validates archived timestamp values",
     withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
       Effect.gen(function* () {
         const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
-        const legacy = yield* createSession(tmp.path, { title: "legacy" })
-        const effect = yield* createSession(tmp.path, { title: "effect" })
+        const session = yield* createSession(tmp.path, { title: "archived" })
         const body = JSON.stringify({ time: { archived: -1 } })
 
-        const legacyResponse = yield* requestWithBackend(
-          false,
-          pathFor(SessionPaths.update, { sessionID: legacy.id }),
-          {
-            method: "PATCH",
-            headers,
-            body,
-          },
-        )
-        expect(legacyResponse.status).toBe(200)
-        expect((yield* json<Session.Info>(legacyResponse)).time.archived).toBe(-1)
-
-        const effectResponse = yield* requestWithBackend(true, pathFor(SessionPaths.update, { sessionID: effect.id }), {
+        const response = yield* request(pathFor(SessionPaths.update, { sessionID: session.id }), {
           method: "PATCH",
           headers,
           body,
         })
-        expect(effectResponse.status).toBe(legacyResponse.status)
-        expect((yield* json<Session.Info>(effectResponse)).time.archived).toBe(-1)
+        expect(response.status).toBe(200)
+        expect((yield* json<Session.Info>(response)).time.archived).toBe(-1)
       }),
     ),
   )
 
   it.live(
-    "matches legacy project-scoped path and directory precedence",
+    "uses project-scoped path and directory precedence",
     withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
       Effect.gen(function* () {
         const currentDir = path.join(tmp.path, "packages", "opencode", "src")
@@ -441,22 +421,18 @@ describe("session HttpApi", () => {
           directory: currentDir,
         })
         const headers = { "x-opencode-directory": tmp.path }
-        const legacy = (yield* json<Session.Info[]>(
-          yield* requestWithBackend(false, `${SessionPaths.list}?${query}`, { headers }),
-        )).map((item) => item.id)
-        const effect = (yield* json<Session.Info[]>(
-          yield* requestWithBackend(true, `${SessionPaths.list}?${query}`, { headers }),
-        )).map((item) => item.id)
-
-        expect(legacy).toContain(pathSession.id)
-        expect(legacy).not.toContain(pathlessSession.id)
-        expect(effect).toEqual(legacy)
+        const sessions = (yield* json<Session.Info[]>(yield* request(`${SessionPaths.list}?${query}`, { headers }))).map(
+          (item) => item.id,
+        )
+
+        expect(sessions).toContain(pathSession.id)
+        expect(sessions).not.toContain(pathlessSession.id)
       }),
     ),
   )
 
   it.live(
-    "matches legacy paginated message link headers",
+    "serves paginated message link headers",
     withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
       Effect.gen(function* () {
         const headers = { "x-opencode-directory": tmp.path }
@@ -465,20 +441,17 @@ describe("session HttpApi", () => {
         yield* createTextMessage(tmp.path, session.id, "second")
         const route = `${pathFor(SessionPaths.messages, { sessionID: session.id })}?limit=1`
 
-        const legacy = yield* requestWithBackend(false, route, { headers })
-        const effect = yield* requestWithBackend(true, route, { headers })
+        const response = yield* request(route, { headers })
 
-        expect(effect.headers.get("x-next-cursor")).toBe(legacy.headers.get("x-next-cursor"))
-        expect(effect.headers.get("link")).toBe(legacy.headers.get("link"))
-        expect(effect.headers.get("access-control-expose-headers")).toBe(
-          legacy.headers.get("access-control-expose-headers"),
-        )
+        expect(response.headers.get("x-next-cursor")).toBeTruthy()
+        expect(response.headers.get("link")).toContain("limit=1")
+        expect(response.headers.get("access-control-expose-headers")?.toLowerCase()).toContain("x-next-cursor")
       }),
     ),
   )
 
   it.live(
-    "serves message mutation routes through Hono bridge",
+    "serves message mutation routes",
     withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
       Effect.gen(function* () {
         const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
@@ -522,7 +495,7 @@ describe("session HttpApi", () => {
   )
 
   it.live(
-    "serves remaining non-LLM session mutation routes through Hono bridge",
+    "serves remaining non-LLM session mutation routes",
     withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
       Effect.gen(function* () {
         const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }

+ 6 - 15
packages/opencode/test/server/httpapi-sync.test.ts

@@ -13,13 +13,11 @@ import { disposeAllInstances, tmpdir } from "../fixture/fixture"
 
 void Log.init({ print: false })
 
-const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
 const context = Context.empty() as Context.Context<unknown>
 
-function app(httpapi = true) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = httpapi
-  return httpapi ? Server.Default().app : Server.Legacy().app
+function app() {
+  return Server.Default().app
 }
 
 function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
@@ -28,14 +26,13 @@ function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
 
 afterEach(async () => {
   mock.restore()
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
   Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
   await disposeAllInstances()
   await resetDatabase()
 })
 
 describe("sync HttpApi", () => {
-  test("serves sync routes through Hono bridge", async () => {
+  test("serves sync routes", async () => {
     Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
     await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
     const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
@@ -87,7 +84,7 @@ describe("sync HttpApi", () => {
     expect(info.mock.calls.some(([message]) => message === "sync replay complete")).toBe(true)
   })
 
-  test("matches legacy seq validation", async () => {
+  test("validates seq values", async () => {
     await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
     const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
     const cases = [
@@ -116,18 +113,12 @@ describe("sync HttpApi", () => {
     ]
 
     for (const item of cases) {
-      const legacy = await app(false).request(item.path, {
+      const response = await app().request(item.path, {
         method: "POST",
         headers,
         body: JSON.stringify(item.body),
       })
-      const httpapi = await app(true).request(item.path, {
-        method: "POST",
-        headers,
-        body: JSON.stringify(item.body),
-      })
-      expect(httpapi.status).toBe(legacy.status)
-      expect(httpapi.status).toBe(400)
+      expect(response.status).toBe(400)
     }
   })
 

+ 0 - 129
packages/opencode/test/server/httpapi-tui.test.ts

@@ -1,129 +0,0 @@
-import { afterEach, describe, expect, test } from "bun:test"
-import type { Context } from "hono"
-import { Flag } from "@opencode-ai/core/flag/flag"
-import { TuiEvent } from "../../src/cli/cmd/tui/event"
-import { SessionID } from "../../src/session/schema"
-import { Instance } from "../../src/project/instance"
-import { TuiApi, TuiPaths } from "../../src/server/routes/instance/httpapi/groups/tui"
-import { callTui } from "../../src/server/routes/instance/tui"
-import { Server } from "../../src/server/server"
-import * as Log from "@opencode-ai/core/util/log"
-import { OpenApi } from "effect/unstable/httpapi"
-import { resetDatabase } from "../fixture/db"
-import { disposeAllInstances, tmpdir } from "../fixture/fixture"
-import { waitGlobalBusEventPromise } from "./global-bus"
-
-void Log.init({ print: false })
-
-const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
-
-function app(experimental = true) {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
-  return experimental ? Server.Default().app : Server.Legacy().app
-}
-
-function nextCommandExecute() {
-  return waitGlobalBusEventPromise({
-    predicate: (event) => event.payload.type === TuiEvent.CommandExecute.type,
-  }).then((event) => event.payload.properties?.command)
-}
-
-async function expectTrue(path: string, headers: Record<string, string>, body?: unknown) {
-  const response = await app().request(path, {
-    method: "POST",
-    headers: { ...headers, "content-type": "application/json" },
-    body: JSON.stringify(body ?? {}),
-  })
-  expect(response.status).toBe(200)
-  expect(await response.json()).toBe(true)
-}
-
-afterEach(async () => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
-  await disposeAllInstances()
-  await resetDatabase()
-})
-
-describe("tui HttpApi bridge", () => {
-  test("documents legacy bad request responses", async () => {
-    const legacy = await Server.openapiHono()
-    const effect = OpenApi.fromApi(TuiApi)
-    for (const path of [TuiPaths.appendPrompt, TuiPaths.executeCommand, TuiPaths.publish, TuiPaths.selectSession]) {
-      expect(legacy.paths[path].post?.responses?.[400]).toBeDefined()
-      expect(effect.paths[path].post?.responses?.[400]).toBeDefined()
-    }
-  })
-
-  test("serves TUI command and event routes through experimental Effect routes", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
-    const headers = { "x-opencode-directory": tmp.path }
-
-    await expectTrue(TuiPaths.appendPrompt, headers, { text: "hello" })
-    await expectTrue(TuiPaths.openHelp, headers)
-    await expectTrue(TuiPaths.openSessions, headers)
-    await expectTrue(TuiPaths.openThemes, headers)
-    await expectTrue(TuiPaths.openModels, headers)
-    await expectTrue(TuiPaths.submitPrompt, headers)
-    await expectTrue(TuiPaths.clearPrompt, headers)
-    await expectTrue(TuiPaths.executeCommand, headers, { command: "agent_cycle" })
-    await expectTrue(TuiPaths.showToast, headers, { message: "Saved", variant: "success" })
-    await expectTrue(TuiPaths.publish, headers, {
-      type: "tui.prompt.append",
-      properties: { text: "from publish" },
-    })
-
-    const missingSessionID = SessionID.descending()
-    const missing = await app().request(TuiPaths.selectSession, {
-      method: "POST",
-      headers: { ...headers, "content-type": "application/json" },
-      body: JSON.stringify({ sessionID: missingSessionID }),
-    })
-    expect(missing.status).toBe(404)
-  })
-
-  test("matches Hono missing selected session error body", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
-    const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
-    const body = JSON.stringify({ sessionID: SessionID.descending() })
-
-    const hono = await app(false).request(TuiPaths.selectSession, { method: "POST", headers, body })
-    const httpapi = await app().request(TuiPaths.selectSession, { method: "POST", headers, body })
-
-    expect(httpapi.status).toBe(hono.status)
-    expect(await httpapi.json()).toEqual(await hono.json())
-  })
-
-  test("matches legacy unknown execute command behavior", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
-    const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
-    const body = JSON.stringify({ command: "unknown_command" })
-
-    const legacyCommand = nextCommandExecute()
-    const legacy = await app(false).request(TuiPaths.executeCommand, { method: "POST", headers, body })
-    expect(legacy.status).toBe(200)
-    expect(await legacy.json()).toBe(true)
-
-    const effectCommand = nextCommandExecute()
-    const effect = await app().request(TuiPaths.executeCommand, { method: "POST", headers, body })
-    expect(effect.status).toBe(200)
-    expect(await effect.json()).toBe(true)
-
-    const legacyPublished = await legacyCommand
-    const effectPublished = await effectCommand
-    expect(effectPublished).toBe(legacyPublished)
-    expect(legacyPublished).toBeUndefined()
-  })
-
-  test("serves TUI control queue through experimental Effect routes", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
-    const pending = callTui({ req: { json: async () => ({ value: 1 }), path: "/demo" } } as unknown as Context)
-    const headers = { "x-opencode-directory": tmp.path }
-
-    const next = await app().request(TuiPaths.controlNext, { headers })
-    expect(next.status).toBe(200)
-    expect(await next.json()).toEqual({ path: "/demo", body: { value: 1 } })
-
-    await expectTrue(TuiPaths.controlResponse, headers, { ok: true })
-    expect(await pending).toEqual({ ok: true })
-  })
-})

+ 0 - 13
packages/opencode/test/server/httpapi-ui.test.ts

@@ -22,7 +22,6 @@ import { Server } from "../../src/server/server"
 void Log.init({ print: false })
 
 const original = {
-  OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
   OPENCODE_DISABLE_EMBEDDED_WEB_UI: Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI,
   OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
   OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
@@ -31,7 +30,6 @@ const original = {
 }
 
 afterEach(() => {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
   Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = original.OPENCODE_DISABLE_EMBEDDED_WEB_UI
   Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
   Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
@@ -117,7 +115,6 @@ function httpClient(response: Response, onRequest?: (request: HttpClientRequest.
 
 describe("HttpApi UI fallback", () => {
   test("serves the web UI through the experimental backend", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
     let proxiedUrl: string | undefined
 
@@ -137,7 +134,6 @@ describe("HttpApi UI fallback", () => {
   })
 
   test("strips upstream transfer encoding headers from proxied assets", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
     let proxiedUrl: string | undefined
 
@@ -189,7 +185,6 @@ describe("HttpApi UI fallback", () => {
   // forwarded through the proxy while the proxy itself re-frames the body,
   // causing browsers to fail with `ERR_INVALID_CHUNKED_ENCODING`.
   test("strips upstream transfer-encoding header from proxied assets", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
 
     const response = await Effect.runPromise(
@@ -232,7 +227,6 @@ describe("HttpApi UI fallback", () => {
   })
 
   test("serves embedded UI assets when Bun can read them but access reports missing", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     let readPath: string | undefined
 
     const response = await Effect.runPromise(
@@ -262,7 +256,6 @@ describe("HttpApi UI fallback", () => {
   })
 
   test("allows embedded UI terminal wasm and theme preload CSP", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     const script = 'document.documentElement.dataset.theme = "dark"'
 
     const response = await Effect.runPromise(
@@ -294,7 +287,6 @@ describe("HttpApi UI fallback", () => {
   })
 
   test("keeps matched API routes ahead of the UI fallback", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
 
     const response = await Server.Default().app.request("/session/nope")
 
@@ -302,7 +294,6 @@ describe("HttpApi UI fallback", () => {
   })
 
   test("requires server password for the web UI", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
 
     const response = await uiApp({ password: "secret", username: "opencode" }).request("/")
@@ -312,7 +303,6 @@ describe("HttpApi UI fallback", () => {
   })
 
   test("accepts auth token for the web UI", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
 
     const response = await uiApp({
@@ -326,7 +316,6 @@ describe("HttpApi UI fallback", () => {
   })
 
   test("accepts basic auth for the web UI", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
 
     const response = await uiApp({ password: "secret", username: "opencode" }).request("/", {
@@ -342,7 +331,6 @@ describe("HttpApi UI fallback", () => {
   // server returning 401 breaks PWA install. These specific public assets
   // should bypass auth.
   test("serves the PWA manifest without auth even when a server password is set", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
 
     for (const path of ["/site.webmanifest", "/web-app-manifest-192x192.png", "/web-app-manifest-512x512.png"]) {
@@ -356,7 +344,6 @@ describe("HttpApi UI fallback", () => {
   })
 
   test("allows web UI preflight without auth", async () => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
 
     const response = await app({ password: "secret", username: "opencode" }).request("/", {
       method: "OPTIONS",

+ 1 - 30
packages/opencode/test/server/httpapi-workspace.test.ts

@@ -24,16 +24,14 @@ import { testEffect } from "../lib/effect"
 void Log.init({ print: false })
 
 const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
-const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 const workspaceLayer = Workspace.defaultLayer.pipe(
   Layer.provide(InstanceStore.defaultLayer),
   Layer.provide(InstanceBootstrap.defaultLayer),
 )
 const it = testEffect(Layer.mergeAll(NodeServices.layer, Project.defaultLayer, Session.defaultLayer, workspaceLayer))
 
-function request(path: string, directory: string, init: RequestInit = {}, httpApi = true) {
+function request(path: string, directory: string, init: RequestInit = {}) {
   return Effect.promise(() => {
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = httpApi
     const headers = new Headers(init.headers)
     headers.set("x-opencode-directory", directory)
     return Promise.resolve(Server.Default().app.request(path, { ...init, headers }))
@@ -161,7 +159,6 @@ function eventStreamResponse() {
 afterEach(async () => {
   mock.restore()
   Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
   await disposeAllInstances()
   await resetDatabase()
 })
@@ -289,32 +286,6 @@ describe("workspace HttpApi", () => {
     }),
   )
 
-  it.live("documents legacy Hono accepting the TUI payload shape", () =>
-    Effect.gen(function* () {
-      Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
-      const dir = yield* tmpdirScoped({ git: true })
-      const project = yield* Project.use.fromDirectory(dir)
-      registerAdapter(project.project.id, "local-test", localAdapter(path.join(dir, ".workspace")))
-
-      const created = yield* request(
-        WorkspacePaths.list,
-        dir,
-        {
-          method: "POST",
-          headers: { "content-type": "application/json" },
-          body: JSON.stringify({ type: "local-test", branch: null }),
-        },
-        false,
-      )
-
-      expect(created.status).toBe(200)
-      expect((yield* Effect.promise(() => created.json())) as Workspace.Info).toMatchObject({
-        type: "local-test",
-        name: "local-test",
-      })
-    }),
-  )
-
   it.live("routes local workspace requests through the workspace target directory", () =>
     Effect.gen(function* () {
       Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true

+ 0 - 76
packages/opencode/test/server/trace-attributes.test.ts

@@ -1,76 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { paramToAttributeKey, requestAttributes } from "../../src/server/routes/instance/trace"
-
-function fakeContext(method: string, url: string, params: Record<string, string>) {
-  return {
-    req: {
-      method,
-      url,
-      param: () => params,
-    },
-  }
-}
-
-describe("paramToAttributeKey", () => {
-  test("converts fooID to foo.id", () => {
-    expect(paramToAttributeKey("sessionID")).toBe("session.id")
-    expect(paramToAttributeKey("messageID")).toBe("message.id")
-    expect(paramToAttributeKey("partID")).toBe("part.id")
-    expect(paramToAttributeKey("projectID")).toBe("project.id")
-    expect(paramToAttributeKey("providerID")).toBe("provider.id")
-    expect(paramToAttributeKey("ptyID")).toBe("pty.id")
-    expect(paramToAttributeKey("permissionID")).toBe("permission.id")
-    expect(paramToAttributeKey("requestID")).toBe("request.id")
-    expect(paramToAttributeKey("workspaceID")).toBe("workspace.id")
-  })
-
-  test("namespaces non-ID params under opencode.", () => {
-    expect(paramToAttributeKey("name")).toBe("opencode.name")
-    expect(paramToAttributeKey("slug")).toBe("opencode.slug")
-  })
-})
-
-describe("requestAttributes", () => {
-  test("includes http method and path", () => {
-    const attrs = requestAttributes(fakeContext("GET", "http://localhost/session", {}))
-    expect(attrs["http.method"]).toBe("GET")
-    expect(attrs["http.path"]).toBe("/session")
-  })
-
-  test("strips query string from path", () => {
-    const attrs = requestAttributes(fakeContext("GET", "http://localhost/file/search?query=foo&limit=10", {}))
-    expect(attrs["http.path"]).toBe("/file/search")
-  })
-
-  test("emits OTel-style <domain>.id for ID-shaped route params", () => {
-    const attrs = requestAttributes(
-      fakeContext("GET", "http://localhost/session/ses_abc/message/msg_def/part/prt_ghi", {
-        sessionID: "ses_abc",
-        messageID: "msg_def",
-        partID: "prt_ghi",
-      }),
-    )
-    expect(attrs["session.id"]).toBe("ses_abc")
-    expect(attrs["message.id"]).toBe("msg_def")
-    expect(attrs["part.id"]).toBe("prt_ghi")
-    // No camelCase leftovers:
-    expect(attrs["opencode.sessionID"]).toBeUndefined()
-    expect(attrs["opencode.messageID"]).toBeUndefined()
-    expect(attrs["opencode.partID"]).toBeUndefined()
-  })
-
-  test("produces no param attributes when no params are matched", () => {
-    const attrs = requestAttributes(fakeContext("POST", "http://localhost/config", {}))
-    expect(Object.keys(attrs).filter((k) => k !== "http.method" && k !== "http.path")).toEqual([])
-  })
-
-  test("namespaces non-ID params under opencode. (e.g. mcp :name)", () => {
-    const attrs = requestAttributes(
-      fakeContext("POST", "http://localhost/mcp/exa/connect", {
-        name: "exa",
-      }),
-    )
-    expect(attrs["opencode.name"]).toBe("exa")
-    expect(attrs["name"]).toBeUndefined()
-  })
-})

+ 0 - 3
packages/opencode/test/server/worktree-endpoint-repro.test.ts

@@ -13,16 +13,13 @@ import { testEffect } from "../lib/effect"
 const stateLayer = Layer.effectDiscard(
   Effect.gen(function* () {
     const original = {
-      OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
       OPENCODE_EXPERIMENTAL_WORKSPACES: Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
     }
 
-    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
     Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
 
     yield* Effect.addFinalizer(() =>
       Effect.promise(async () => {
-        Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
         Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = original.OPENCODE_EXPERIMENTAL_WORKSPACES
         await resetDatabase()
       }),

+ 1 - 8
packages/sdk/js/script/build.ts

@@ -9,16 +9,9 @@ import path from "path"
 
 import { createClient } from "@hey-api/openapi-ts"
 
-const openapiSource = process.env.OPENCODE_SDK_OPENAPI === "hono" ? "hono" : "httpapi"
 const opencode = path.resolve(dir, "../../opencode")
 
-// `bun dev generate` now derives the spec from the Effect HttpApi contract by
-// default; pass `--hono` to fall back to the legacy Hono spec for parity diffs.
-if (openapiSource === "httpapi") {
-  await $`bun dev generate > ${dir}/openapi.json`.cwd(opencode)
-} else {
-  await $`bun dev generate --hono > ${dir}/openapi.json`.cwd(opencode)
-}
+await $`bun dev generate > ${dir}/openapi.json`.cwd(opencode)
 
 await createClient({
   input: "./openapi.json",