fix(httpapi): expose unavailable v2 session mutations (#28624)
This commit is contained in:
@@ -4,7 +4,7 @@ import { Prompt } from "@opencode-ai/core/session-prompt"
|
|||||||
import { SessionV2 } from "@/v2/session"
|
import { SessionV2 } from "@/v2/session"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
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 { V2Authorization } from "../../middleware/authorization"
|
||||||
import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
|
import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
|
||||||
import { QueryBoolean } from "../query"
|
import { QueryBoolean } from "../query"
|
||||||
@@ -61,7 +61,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||||||
delivery: SessionV2.Delivery.pipe(Schema.optional),
|
delivery: SessionV2.Delivery.pipe(Schema.optional),
|
||||||
}),
|
}),
|
||||||
success: SessionMessage.Message,
|
success: SessionMessage.Message,
|
||||||
error: SessionNotFoundError,
|
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "v2.session.prompt",
|
identifier: "v2.session.prompt",
|
||||||
@@ -75,7 +75,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
success: HttpApiSchema.NoContent,
|
success: HttpApiSchema.NoContent,
|
||||||
error: SessionNotFoundError,
|
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "v2.session.compact",
|
identifier: "v2.session.compact",
|
||||||
@@ -89,7 +89,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
success: HttpApiSchema.NoContent,
|
success: HttpApiSchema.NoContent,
|
||||||
error: SessionNotFoundError,
|
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "v2.session.wait",
|
identifier: "v2.session.wait",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SessionV2 } from "@/v2/session"
|
|||||||
import { DateTime, Effect, Option, Schema } from "effect"
|
import { DateTime, Effect, Option, Schema } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
import { InstanceHttpApi } from "../../api"
|
import { InstanceHttpApi } from "../../api"
|
||||||
import { InvalidCursorError, InvalidRequestError, SessionNotFoundError } from "../../errors"
|
import { InvalidCursorError, InvalidRequestError, ServiceUnavailableError, SessionNotFoundError } from "../../errors"
|
||||||
|
|
||||||
const DefaultSessionsLimit = 50
|
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()
|
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()
|
return HttpApiSchema.NoContent.make()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Ses
|
|||||||
sessionID: SessionID,
|
sessionID: SessionID,
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
|
export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
|
||||||
|
"Session.OperationUnavailableError",
|
||||||
|
{
|
||||||
|
operation: Schema.Literals(["prompt", "compact", "wait"]),
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly create: (input?: {
|
readonly create: (input?: {
|
||||||
agent?: string
|
agent?: string
|
||||||
@@ -104,7 +111,7 @@ export interface Interface {
|
|||||||
sessionID: SessionID
|
sessionID: SessionID
|
||||||
prompt: Prompt
|
prompt: Prompt
|
||||||
delivery?: Delivery
|
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 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 skill: (input: { id?: EventV2.ID; sessionID: SessionID; skill: string }) => Effect.Effect<void, never>
|
||||||
readonly subagent: (input: {
|
readonly subagent: (input: {
|
||||||
@@ -113,11 +120,11 @@ export interface Interface {
|
|||||||
prompt: Prompt
|
prompt: Prompt
|
||||||
agent: string
|
agent: string
|
||||||
model?: ModelV2.Ref
|
model?: ModelV2.Ref
|
||||||
}) => Effect.Effect<void, NotFoundError>
|
}) => Effect.Effect<void, NotFoundError | OperationUnavailableError>
|
||||||
readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect<void, never>
|
readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect<void, never>
|
||||||
readonly switchModel: (input: { sessionID: SessionID; model: ModelV2.Ref }) => 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 compact: (sessionID: SessionID) => Effect.Effect<void, NotFoundError | OperationUnavailableError>
|
||||||
readonly wait: (sessionID: SessionID) => Effect.Effect<void, NotFoundError>
|
readonly wait: (sessionID: SessionID) => Effect.Effect<void, NotFoundError | OperationUnavailableError>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
|
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) {
|
prompt: Effect.fn("V2Session.prompt")(function* (input) {
|
||||||
yield* result.get(input.sessionID)
|
yield* result.get(input.sessionID)
|
||||||
return {} as any
|
return yield* new OperationUnavailableError({ operation: "prompt" })
|
||||||
}),
|
}),
|
||||||
shell: Effect.fn("V2Session.shell")(function* (_input) {}),
|
shell: Effect.fn("V2Session.shell")(function* (_input) {}),
|
||||||
skill: Effect.fn("V2Session.skill")(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) {
|
compact: Effect.fn("V2Session.compact")(function* (sessionID) {
|
||||||
yield* result.get(sessionID)
|
yield* result.get(sessionID)
|
||||||
|
return yield* new OperationUnavailableError({ operation: "compact" })
|
||||||
}),
|
}),
|
||||||
wait: Effect.fn("V2Session.wait")(function* (sessionID) {
|
wait: Effect.fn("V2Session.wait")(function* (sessionID) {
|
||||||
yield* result.get(sessionID)
|
yield* result.get(sessionID)
|
||||||
|
return yield* new OperationUnavailableError({ operation: "wait" })
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -442,6 +442,45 @@ describe("session HttpApi", () => {
|
|||||||
{ git: true, config: { formatter: false, lsp: false } },
|
{ 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(
|
it.instance(
|
||||||
"serves sessions with migrated summary diffs missing file details",
|
"serves sessions with migrated summary diffs missing file details",
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user