Browse Source

fix(httpapi): expose unavailable v2 session mutations (#28624)

Shoubhit Dash 3 tháng trước cách đây
mục cha
commit
f5d20c580b

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

@@ -4,7 +4,7 @@ import { Prompt } from "@opencode-ai/core/session-prompt"
 import { SessionV2 } from "@/v2/session"
 import { Schema } from "effect"
 import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
-import { InvalidCursorError, InvalidRequestError, SessionNotFoundError } from "../../errors"
+import { InvalidCursorError, InvalidRequestError, ServiceUnavailableError, SessionNotFoundError } from "../../errors"
 import { V2Authorization } from "../../middleware/authorization"
 import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
 import { QueryBoolean } from "../query"
@@ -61,7 +61,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
         delivery: SessionV2.Delivery.pipe(Schema.optional),
       }),
       success: SessionMessage.Message,
-      error: SessionNotFoundError,
+      error: [SessionNotFoundError, ServiceUnavailableError],
     }).annotateMerge(
       OpenApi.annotations({
         identifier: "v2.session.prompt",
@@ -75,7 +75,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
       params: { sessionID: SessionID },
       query: WorkspaceRoutingQuery,
       success: HttpApiSchema.NoContent,
-      error: SessionNotFoundError,
+      error: [SessionNotFoundError, ServiceUnavailableError],
     }).annotateMerge(
       OpenApi.annotations({
         identifier: "v2.session.compact",
@@ -89,7 +89,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
       params: { sessionID: SessionID },
       query: WorkspaceRoutingQuery,
       success: HttpApiSchema.NoContent,
-      error: SessionNotFoundError,
+      error: [SessionNotFoundError, ServiceUnavailableError],
     }).annotateMerge(
       OpenApi.annotations({
         identifier: "v2.session.wait",

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

@@ -3,7 +3,7 @@ import { SessionV2 } from "@/v2/session"
 import { DateTime, Effect, Option, Schema } from "effect"
 import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../../api"
-import { InvalidCursorError, InvalidRequestError, SessionNotFoundError } from "../../errors"
+import { InvalidCursorError, InvalidRequestError, ServiceUnavailableError, SessionNotFoundError } from "../../errors"
 
 const DefaultSessionsLimit = 50
 
@@ -148,6 +148,14 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
                   }),
                 ),
               ),
+              Effect.catchTag("Session.OperationUnavailableError", (error) =>
+                Effect.fail(
+                  new ServiceUnavailableError({
+                    message: `V2 session ${error.operation} is not available yet`,
+                    service: `v2.session.${error.operation}`,
+                  }),
+                ),
+              ),
             )
         }),
       )
@@ -163,6 +171,14 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
                 }),
               ),
             ),
+            Effect.catchTag("Session.OperationUnavailableError", (error) =>
+              Effect.fail(
+                new ServiceUnavailableError({
+                  message: `V2 session ${error.operation} is not available yet`,
+                  service: `v2.session.${error.operation}`,
+                }),
+              ),
+            ),
           )
           return HttpApiSchema.NoContent.make()
         }),
@@ -179,6 +195,14 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
                 }),
               ),
             ),
+            Effect.catchTag("Session.OperationUnavailableError", (error) =>
+              Effect.fail(
+                new ServiceUnavailableError({
+                  message: `V2 session ${error.operation} is not available yet`,
+                  service: `v2.session.${error.operation}`,
+                }),
+              ),
+            ),
           )
           return HttpApiSchema.NoContent.make()
         }),

+ 14 - 5
packages/opencode/src/v2/session.ts

@@ -65,6 +65,13 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Ses
   sessionID: SessionID,
 }) {}
 
+export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
+  "Session.OperationUnavailableError",
+  {
+    operation: Schema.Literals(["prompt", "compact", "wait"]),
+  },
+) {}
+
 export interface Interface {
   readonly create: (input?: {
     agent?: string
@@ -104,7 +111,7 @@ export interface Interface {
     sessionID: SessionID
     prompt: Prompt
     delivery?: Delivery
-  }) => Effect.Effect<SessionMessage.User, NotFoundError>
+  }) => Effect.Effect<SessionMessage.User, NotFoundError | OperationUnavailableError>
   readonly shell: (input: { id?: EventV2.ID; sessionID: SessionID; command: string }) => Effect.Effect<void, never>
   readonly skill: (input: { id?: EventV2.ID; sessionID: SessionID; skill: string }) => Effect.Effect<void, never>
   readonly subagent: (input: {
@@ -113,11 +120,11 @@ export interface Interface {
     prompt: Prompt
     agent: string
     model?: ModelV2.Ref
-  }) => Effect.Effect<void, NotFoundError>
+  }) => Effect.Effect<void, NotFoundError | OperationUnavailableError>
   readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect<void, never>
   readonly switchModel: (input: { sessionID: SessionID; model: ModelV2.Ref }) => Effect.Effect<void, never>
-  readonly compact: (sessionID: SessionID) => Effect.Effect<void, NotFoundError>
-  readonly wait: (sessionID: SessionID) => Effect.Effect<void, NotFoundError>
+  readonly compact: (sessionID: SessionID) => Effect.Effect<void, NotFoundError | OperationUnavailableError>
+  readonly wait: (sessionID: SessionID) => Effect.Effect<void, NotFoundError | OperationUnavailableError>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
@@ -292,7 +299,7 @@ export const layer = Layer.effect(
       }),
       prompt: Effect.fn("V2Session.prompt")(function* (input) {
         yield* result.get(input.sessionID)
-        return {} as any
+        return yield* new OperationUnavailableError({ operation: "prompt" })
       }),
       shell: Effect.fn("V2Session.shell")(function* (_input) {}),
       skill: Effect.fn("V2Session.skill")(function* (_input) {}),
@@ -333,9 +340,11 @@ export const layer = Layer.effect(
       }),
       compact: Effect.fn("V2Session.compact")(function* (sessionID) {
         yield* result.get(sessionID)
+        return yield* new OperationUnavailableError({ operation: "compact" })
       }),
       wait: Effect.fn("V2Session.wait")(function* (sessionID) {
         yield* result.get(sessionID)
+        return yield* new OperationUnavailableError({ operation: "wait" })
       }),
     })
 

+ 14 - 0
packages/opencode/test/server/httpapi-public-openapi.test.ts

@@ -115,4 +115,18 @@ describe("PublicApi OpenAPI v2 errors", () => {
       )
     }
   })
+
+  test("documents v2 unfinished session mutation errors", () => {
+    const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
+
+    for (const route of [
+      ["post", "/api/session/{sessionID}/prompt"],
+      ["post", "/api/session/{sessionID}/compact"],
+      ["post", "/api/session/{sessionID}/wait"],
+    ] as const) {
+      expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["503"]) ?? "")).toBe(
+        "ServiceUnavailableError",
+      )
+    }
+  })
 })

+ 39 - 0
packages/opencode/test/server/httpapi-session.test.ts

@@ -442,6 +442,45 @@ describe("session HttpApi", () => {
     { git: true, config: { formatter: false, lsp: false } },
   )
 
+  it.instance(
+    "returns v2 public unavailable errors for unfinished session mutations",
+    () =>
+      Effect.gen(function* () {
+        const test = yield* TestInstance
+        const headers = { "x-opencode-directory": test.directory }
+        const session = yield* createSession({ title: "v2 unavailable" })
+
+        const prompt = yield* request(`/api/session/${session.id}/prompt`, {
+          method: "POST",
+          headers: { ...headers, "content-type": "application/json" },
+          body: JSON.stringify({ prompt: { text: "hello" } }),
+        })
+        expect(prompt.status).toBe(503)
+        expect(yield* responseJson(prompt)).toEqual({
+          _tag: "ServiceUnavailableError",
+          message: "V2 session prompt is not available yet",
+          service: "v2.session.prompt",
+        })
+
+        const compact = yield* request(`/api/session/${session.id}/compact`, { method: "POST", headers })
+        expect(compact.status).toBe(503)
+        expect(yield* responseJson(compact)).toEqual({
+          _tag: "ServiceUnavailableError",
+          message: "V2 session compact is not available yet",
+          service: "v2.session.compact",
+        })
+
+        const wait = yield* request(`/api/session/${session.id}/wait`, { method: "POST", headers })
+        expect(wait.status).toBe(503)
+        expect(yield* responseJson(wait)).toEqual({
+          _tag: "ServiceUnavailableError",
+          message: "V2 session wait is not available yet",
+          service: "v2.session.wait",
+        })
+      }),
+    { git: true, config: { formatter: false, lsp: false } },
+  )
+
   it.instance(
     "serves sessions with migrated summary diffs missing file details",
     () =>