diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts
index b8c8a142be..7f7d8d1118 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts
@@ -19,7 +19,7 @@ import {
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
-import { ApiNotFoundError } from "../errors"
+import { ApiNotFoundError, SessionBusyError } from "../errors"
import { described } from "./metadata"
import { QueryBoolean } from "./query"
@@ -354,7 +354,7 @@ export const SessionApi = HttpApi.make("session")
query: WorkspaceRoutingQuery,
payload: ShellPayload,
success: described(MessageV2.WithParts, "Created message"),
- error: [HttpApiError.BadRequest, ApiNotFoundError],
+ error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.shell",
@@ -367,7 +367,7 @@ export const SessionApi = HttpApi.make("session")
query: WorkspaceRoutingQuery,
payload: RevertPayload,
success: described(Session.Info, "Updated session"),
- error: [HttpApiError.BadRequest, ApiNotFoundError],
+ error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.revert",
@@ -380,7 +380,7 @@ export const SessionApi = HttpApi.make("session")
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Updated session"),
- error: [HttpApiError.BadRequest, ApiNotFoundError],
+ error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.unrevert",
@@ -406,7 +406,7 @@ export const SessionApi = HttpApi.make("session")
params: { sessionID: SessionID, messageID: MessageID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Successfully deleted message"),
- error: [HttpApiError.BadRequest, ApiNotFoundError],
+ error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.deleteMessage",
diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts
index d4ab0eb599..e297bdd0a8 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts
@@ -1,7 +1,6 @@
import type { NotFoundError as StorageNotFoundError } from "@/storage/storage"
import type { Session } from "@/session/session"
import { Effect } from "effect"
-import { HttpApiError } from "effect/unstable/httpapi"
import * as ApiError from "../errors"
export function mapStorageNotFound(self: Effect.Effect) {
@@ -9,5 +8,14 @@ export function mapStorageNotFound(self: Effect.Effect(self: Effect.Effect) {
- return self.pipe(Effect.catchTag("SessionBusyError", () => Effect.fail(new HttpApiError.BadRequest({}))))
+ return self.pipe(
+ Effect.catchTag("SessionBusyError", (error) =>
+ Effect.fail(
+ new ApiError.SessionBusyError({
+ sessionID: error.sessionID,
+ message: `Session is busy: ${error.sessionID}`,
+ }),
+ ),
+ ),
+ )
}
diff --git a/packages/opencode/test/server/httpapi-public-openapi.test.ts b/packages/opencode/test/server/httpapi-public-openapi.test.ts
index bd3b8c18ec..547beb9799 100644
--- a/packages/opencode/test/server/httpapi-public-openapi.test.ts
+++ b/packages/opencode/test/server/httpapi-public-openapi.test.ts
@@ -142,4 +142,19 @@ describe("PublicApi OpenAPI v2 errors", () => {
)
}
})
+
+ test("documents session busy errors", () => {
+ const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
+
+ for (const route of [
+ ["post", "/session/{sessionID}/shell"],
+ ["post", "/session/{sessionID}/revert"],
+ ["post", "/session/{sessionID}/unrevert"],
+ ["delete", "/session/{sessionID}/message/{messageID}"],
+ ] as const) {
+ expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["409"]) ?? "")).toBe(
+ "SessionBusyError",
+ )
+ }
+ })
})
diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts
index cdb50227ef..a13f986b4d 100644
--- a/packages/opencode/test/server/httpapi-session.test.ts
+++ b/packages/opencode/test/server/httpapi-session.test.ts
@@ -1,7 +1,7 @@
import { afterEach, describe, expect } from "bun:test"
import { mkdir } from "node:fs/promises"
import path from "node:path"
-import { Effect, Layer } from "effect"
+import { Cause, Effect, Exit, Layer } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdapter } from "../../src/control-plane/types"
@@ -13,6 +13,7 @@ import { InstanceBootstrap as InstanceBootstrapService } from "../../src/project
import { InstanceStore } from "../../src/project/instance-store"
import { Project } from "../../src/project/project"
import { Server } from "../../src/server/server"
+import * as HttpSessionError from "../../src/server/routes/instance/httpapi/handlers/session-errors"
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
import { Session } from "@/session/session"
import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema"
@@ -215,6 +216,22 @@ afterEach(async () => {
})
describe("session HttpApi", () => {
+ it.effect("maps busy sessions to public session busy errors", () =>
+ Effect.gen(function* () {
+ const sessionID = SessionID.descending()
+ const exit = yield* HttpSessionError.mapBusy(Effect.fail(new Session.BusyError({ sessionID }))).pipe(Effect.exit)
+
+ expect(Exit.isFailure(exit)).toBe(true)
+ if (Exit.isFailure(exit)) {
+ expect(Cause.squash(exit.cause)).toMatchObject({
+ _tag: "SessionBusyError",
+ sessionID,
+ message: `Session is busy: ${sessionID}`,
+ })
+ }
+ }),
+ )
+
it.instance(
"returns declared not found errors for read routes",
() =>