mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 08:46:15 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98fef45553 |
@@ -0,0 +1,21 @@
|
|||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 404 Not Found error matching the legacy Hono `NamedError` JSON shape:
|
||||||
|
* `{ name: "NotFoundError", data: { message } }`.
|
||||||
|
*
|
||||||
|
* `httpApiStatus: 404` annotation drives the response status; the schema
|
||||||
|
* fields drive the response body. Use this in place of
|
||||||
|
* `HttpApiError.NotFound` (which has an empty body) anywhere SDK clients
|
||||||
|
* may inspect `error.data.message`.
|
||||||
|
*/
|
||||||
|
export class OpencodeNotFound extends Schema.ErrorClass<OpencodeNotFound>("opencode/Error/NotFound")(
|
||||||
|
{
|
||||||
|
name: Schema.tag("NotFoundError"),
|
||||||
|
data: Schema.Struct({ message: Schema.String }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Not found",
|
||||||
|
httpApiStatus: 404,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
@@ -12,6 +12,7 @@ import { MessageID, PartID, SessionID } from "@/session/schema"
|
|||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { Schema, SchemaGetter, Struct } from "effect"
|
import { Schema, SchemaGetter, Struct } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { OpencodeNotFound } from "../errors"
|
||||||
import { Authorization } from "../middleware/authorization"
|
import { Authorization } from "../middleware/authorization"
|
||||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||||
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
||||||
@@ -123,7 +124,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.get("get", SessionPaths.get, {
|
HttpApiEndpoint.get("get", SessionPaths.get, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
success: described(Session.Info, "Get session"),
|
success: described(Session.Info, "Get session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.get",
|
identifier: "session.get",
|
||||||
@@ -134,7 +135,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.get("children", SessionPaths.children, {
|
HttpApiEndpoint.get("children", SessionPaths.children, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
success: described(Schema.Array(Session.Info), "List of children"),
|
success: described(Schema.Array(Session.Info), "List of children"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.children",
|
identifier: "session.children",
|
||||||
@@ -145,7 +146,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.get("todo", SessionPaths.todo, {
|
HttpApiEndpoint.get("todo", SessionPaths.todo, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
success: described(Schema.Array(Todo.Info), "Todo list"),
|
success: described(Schema.Array(Todo.Info), "Todo list"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.todo",
|
identifier: "session.todo",
|
||||||
@@ -157,6 +158,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
query: DiffQuery,
|
query: DiffQuery,
|
||||||
success: described(Schema.Array(Snapshot.FileDiff), "Successfully retrieved diff"),
|
success: described(Schema.Array(Snapshot.FileDiff), "Successfully retrieved diff"),
|
||||||
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.diff",
|
identifier: "session.diff",
|
||||||
@@ -168,7 +170,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
query: MessagesQuery,
|
query: MessagesQuery,
|
||||||
success: described(Schema.Array(MessageV2.WithParts), "List of messages"),
|
success: described(Schema.Array(MessageV2.WithParts), "List of messages"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.messages",
|
identifier: "session.messages",
|
||||||
@@ -179,7 +181,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.get("message", SessionPaths.message, {
|
HttpApiEndpoint.get("message", SessionPaths.message, {
|
||||||
params: { sessionID: SessionID, messageID: MessageID },
|
params: { sessionID: SessionID, messageID: MessageID },
|
||||||
success: described(MessageV2.WithParts, "Message"),
|
success: described(MessageV2.WithParts, "Message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.message",
|
identifier: "session.message",
|
||||||
@@ -201,7 +203,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
|
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
success: described(Schema.Boolean, "Successfully deleted session"),
|
success: described(Schema.Boolean, "Successfully deleted session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.delete",
|
identifier: "session.delete",
|
||||||
@@ -213,7 +215,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: UpdatePayload,
|
payload: UpdatePayload,
|
||||||
success: described(Session.Info, "Successfully updated session"),
|
success: described(Session.Info, "Successfully updated session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.update",
|
identifier: "session.update",
|
||||||
@@ -225,6 +227,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: ForkPayload,
|
payload: ForkPayload,
|
||||||
success: described(Session.Info, "200"),
|
success: described(Session.Info, "200"),
|
||||||
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.fork",
|
identifier: "session.fork",
|
||||||
@@ -235,7 +238,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
success: described(Schema.Boolean, "Aborted session"),
|
success: described(Schema.Boolean, "Aborted session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.abort",
|
identifier: "session.abort",
|
||||||
@@ -247,7 +250,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: InitPayload,
|
payload: InitPayload,
|
||||||
success: described(Schema.Boolean, "200"),
|
success: described(Schema.Boolean, "200"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.init",
|
identifier: "session.init",
|
||||||
@@ -259,7 +262,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.post("share", SessionPaths.share, {
|
HttpApiEndpoint.post("share", SessionPaths.share, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
success: described(Session.Info, "Successfully shared session"),
|
success: described(Session.Info, "Successfully shared session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.share",
|
identifier: "session.share",
|
||||||
@@ -270,7 +273,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
|
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
success: described(Session.Info, "Successfully unshared session"),
|
success: described(Session.Info, "Successfully unshared session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.unshare",
|
identifier: "session.unshare",
|
||||||
@@ -282,7 +285,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: SummarizePayload,
|
payload: SummarizePayload,
|
||||||
success: described(Schema.Boolean, "Summarized session"),
|
success: described(Schema.Boolean, "Summarized session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.summarize",
|
identifier: "session.summarize",
|
||||||
@@ -294,7 +297,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: PromptPayload,
|
payload: PromptPayload,
|
||||||
success: described(MessageV2.WithParts, "Created message"),
|
success: described(MessageV2.WithParts, "Created message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.prompt",
|
identifier: "session.prompt",
|
||||||
@@ -306,7 +309,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: PromptPayload,
|
payload: PromptPayload,
|
||||||
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
|
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.prompt_async",
|
identifier: "session.prompt_async",
|
||||||
@@ -319,7 +322,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: CommandPayload,
|
payload: CommandPayload,
|
||||||
success: described(MessageV2.WithParts, "Created message"),
|
success: described(MessageV2.WithParts, "Created message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.command",
|
identifier: "session.command",
|
||||||
@@ -331,7 +334,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: ShellPayload,
|
payload: ShellPayload,
|
||||||
success: described(MessageV2.WithParts, "Created message"),
|
success: described(MessageV2.WithParts, "Created message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.shell",
|
identifier: "session.shell",
|
||||||
@@ -343,7 +346,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
payload: RevertPayload,
|
payload: RevertPayload,
|
||||||
success: described(Session.Info, "Updated session"),
|
success: described(Session.Info, "Updated session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.revert",
|
identifier: "session.revert",
|
||||||
@@ -355,7 +358,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
|
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
|
||||||
params: { sessionID: SessionID },
|
params: { sessionID: SessionID },
|
||||||
success: described(Session.Info, "Updated session"),
|
success: described(Session.Info, "Updated session"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.unrevert",
|
identifier: "session.unrevert",
|
||||||
@@ -367,7 +370,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID, permissionID: PermissionID },
|
params: { sessionID: SessionID, permissionID: PermissionID },
|
||||||
payload: PermissionResponsePayload,
|
payload: PermissionResponsePayload,
|
||||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "permission.respond",
|
identifier: "permission.respond",
|
||||||
@@ -379,7 +382,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
|
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
|
||||||
params: { sessionID: SessionID, messageID: MessageID },
|
params: { sessionID: SessionID, messageID: MessageID },
|
||||||
success: described(Schema.Boolean, "Successfully deleted message"),
|
success: described(Schema.Boolean, "Successfully deleted message"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "session.deleteMessage",
|
identifier: "session.deleteMessage",
|
||||||
@@ -391,7 +394,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
|
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
|
||||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||||
success: described(Schema.Boolean, "Successfully deleted part"),
|
success: described(Schema.Boolean, "Successfully deleted part"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "part.delete",
|
identifier: "part.delete",
|
||||||
@@ -402,7 +405,7 @@ export const SessionApi = HttpApi.make("session")
|
|||||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||||
payload: MessageV2.Part,
|
payload: MessageV2.Part,
|
||||||
success: described(MessageV2.Part, "Successfully updated part"),
|
success: described(MessageV2.Part, "Successfully updated part"),
|
||||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
error: [HttpApiError.BadRequest, OpencodeNotFound],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
identifier: "part.update",
|
identifier: "part.update",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { SessionSummary } from "@/session/summary"
|
|||||||
import { Todo } from "@/session/todo"
|
import { Todo } from "@/session/todo"
|
||||||
import { MessageID, PartID, SessionID } from "@/session/schema"
|
import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||||
import { NotFoundError } from "@/storage/storage"
|
import { NotFoundError } from "@/storage/storage"
|
||||||
|
import { OpencodeNotFound } from "../errors"
|
||||||
import { NamedError } from "@opencode-ai/core/util/error"
|
import { NamedError } from "@opencode-ai/core/util/error"
|
||||||
import { Cause, Effect, Option, Schema, Scope } from "effect"
|
import { Cause, Effect, Option, Schema, Scope } from "effect"
|
||||||
import * as Stream from "effect/Stream"
|
import * as Stream from "effect/Stream"
|
||||||
@@ -38,11 +39,17 @@ import {
|
|||||||
UpdatePayload,
|
UpdatePayload,
|
||||||
} from "../groups/session"
|
} from "../groups/session"
|
||||||
|
|
||||||
|
// TODO: long-term, services like Session.Service should fail with typed errors
|
||||||
|
// directly (e.g. Effect<SessionInfo, SessionNotFound>) and let HttpApi auto-route
|
||||||
|
// status + body via the schema annotations. Until then, we catch the legacy
|
||||||
|
// thrown NotFoundError at the boundary and rebrand to OpencodeNotFound — which
|
||||||
|
// matches the Hono NamedError JSON shape SDK consumers already expect.
|
||||||
const mapNotFound = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
const mapNotFound = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||||
self.pipe(
|
self.pipe(
|
||||||
Effect.catchIf(NotFoundError.isInstance, () => Effect.fail(new HttpApiError.NotFound({}))),
|
|
||||||
Effect.catchDefect((error) =>
|
Effect.catchDefect((error) =>
|
||||||
NotFoundError.isInstance(error) ? Effect.fail(new HttpApiError.NotFound({})) : Effect.die(error),
|
NotFoundError.isInstance(error)
|
||||||
|
? Effect.fail(new OpencodeNotFound({ data: { message: error.message } }))
|
||||||
|
: Effect.die(error),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -87,14 +94,14 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|||||||
})
|
})
|
||||||
|
|
||||||
const todo = Effect.fn("SessionHttpApi.todo")(function* (ctx: { params: { sessionID: SessionID } }) {
|
const todo = Effect.fn("SessionHttpApi.todo")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||||
return yield* todoSvc.get(ctx.params.sessionID)
|
return yield* mapNotFound(todoSvc.get(ctx.params.sessionID))
|
||||||
})
|
})
|
||||||
|
|
||||||
const diff = Effect.fn("SessionHttpApi.diff")(function* (ctx: {
|
const diff = Effect.fn("SessionHttpApi.diff")(function* (ctx: {
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
query: typeof DiffQuery.Type
|
query: typeof DiffQuery.Type
|
||||||
}) {
|
}) {
|
||||||
return yield* summary.diff({ sessionID: ctx.params.sessionID, messageID: ctx.query.messageID })
|
return yield* mapNotFound(summary.diff({ sessionID: ctx.params.sessionID, messageID: ctx.query.messageID }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const messages = Effect.fn("SessionHttpApi.messages")(function* (ctx: {
|
const messages = Effect.fn("SessionHttpApi.messages")(function* (ctx: {
|
||||||
@@ -198,11 +205,11 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof ForkPayload.Type
|
payload: typeof ForkPayload.Type
|
||||||
}) {
|
}) {
|
||||||
return yield* session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID })
|
return yield* mapNotFound(session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const abort = Effect.fn("SessionHttpApi.abort")(function* (ctx: { params: { sessionID: SessionID } }) {
|
const abort = Effect.fn("SessionHttpApi.abort")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||||
yield* promptSvc.cancel(ctx.params.sessionID)
|
yield* mapNotFound(promptSvc.cancel(ctx.params.sessionID))
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -210,13 +217,15 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof InitPayload.Type
|
payload: typeof InitPayload.Type
|
||||||
}) {
|
}) {
|
||||||
yield* promptSvc.command({
|
yield* mapNotFound(
|
||||||
sessionID: ctx.params.sessionID,
|
promptSvc.command({
|
||||||
messageID: ctx.payload.messageID,
|
sessionID: ctx.params.sessionID,
|
||||||
model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
|
messageID: ctx.payload.messageID,
|
||||||
command: Command.Default.INIT,
|
model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
|
||||||
arguments: "",
|
command: Command.Default.INIT,
|
||||||
})
|
arguments: "",
|
||||||
|
}),
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -234,22 +243,26 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof SummarizePayload.Type
|
payload: typeof SummarizePayload.Type
|
||||||
}) {
|
}) {
|
||||||
yield* revertSvc.cleanup(yield* session.get(ctx.params.sessionID))
|
return yield* mapNotFound(
|
||||||
const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
|
Effect.gen(function* () {
|
||||||
const defaultAgent = yield* agentSvc.defaultAgent()
|
yield* revertSvc.cleanup(yield* session.get(ctx.params.sessionID))
|
||||||
const currentAgent = messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
|
const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
|
||||||
|
const defaultAgent = yield* agentSvc.defaultAgent()
|
||||||
|
const currentAgent = messages.findLast((m) => m.info.role === "user")?.info.agent ?? defaultAgent
|
||||||
|
|
||||||
yield* compactSvc.create({
|
yield* compactSvc.create({
|
||||||
sessionID: ctx.params.sessionID,
|
sessionID: ctx.params.sessionID,
|
||||||
agent: currentAgent,
|
agent: currentAgent,
|
||||||
model: {
|
model: {
|
||||||
providerID: ctx.payload.providerID,
|
providerID: ctx.payload.providerID,
|
||||||
modelID: ctx.payload.modelID,
|
modelID: ctx.payload.modelID,
|
||||||
},
|
},
|
||||||
auto: ctx.payload.auto ?? false,
|
auto: ctx.payload.auto ?? false,
|
||||||
})
|
})
|
||||||
yield* promptSvc.loop({ sessionID: ctx.params.sessionID })
|
yield* promptSvc.loop({ sessionID: ctx.params.sessionID })
|
||||||
return true
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const prompt = Effect.fn("SessionHttpApi.prompt")(function* (ctx: {
|
const prompt = Effect.fn("SessionHttpApi.prompt")(function* (ctx: {
|
||||||
@@ -297,25 +310,25 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof CommandPayload.Type
|
payload: typeof CommandPayload.Type
|
||||||
}) {
|
}) {
|
||||||
return yield* promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID })
|
return yield* mapNotFound(promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const shell = Effect.fn("SessionHttpApi.shell")(function* (ctx: {
|
const shell = Effect.fn("SessionHttpApi.shell")(function* (ctx: {
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof ShellPayload.Type
|
payload: typeof ShellPayload.Type
|
||||||
}) {
|
}) {
|
||||||
return yield* promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID })
|
return yield* mapNotFound(promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const revert = Effect.fn("SessionHttpApi.revert")(function* (ctx: {
|
const revert = Effect.fn("SessionHttpApi.revert")(function* (ctx: {
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof RevertPayload.Type
|
payload: typeof RevertPayload.Type
|
||||||
}) {
|
}) {
|
||||||
return yield* revertSvc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload })
|
return yield* mapNotFound(revertSvc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const unrevert = Effect.fn("SessionHttpApi.unrevert")(function* (ctx: { params: { sessionID: SessionID } }) {
|
const unrevert = Effect.fn("SessionHttpApi.unrevert")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||||
return yield* revertSvc.unrevert({ sessionID: ctx.params.sessionID })
|
return yield* mapNotFound(revertSvc.unrevert({ sessionID: ctx.params.sessionID }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
|
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
|
||||||
@@ -329,8 +342,12 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
|||||||
const deleteMessage = Effect.fn("SessionHttpApi.deleteMessage")(function* (ctx: {
|
const deleteMessage = Effect.fn("SessionHttpApi.deleteMessage")(function* (ctx: {
|
||||||
params: { sessionID: SessionID; messageID: MessageID }
|
params: { sessionID: SessionID; messageID: MessageID }
|
||||||
}) {
|
}) {
|
||||||
yield* runState.assertNotBusy(ctx.params.sessionID)
|
yield* mapNotFound(
|
||||||
yield* session.removeMessage(ctx.params)
|
Effect.gen(function* () {
|
||||||
|
yield* runState.assertNotBusy(ctx.params.sessionID)
|
||||||
|
yield* session.removeMessage(ctx.params)
|
||||||
|
}),
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
|
|||||||
function createSessionWithMessages(directory: string, count: number) {
|
function createSessionWithMessages(directory: string, count: number) {
|
||||||
return WithInstance.provide({
|
return WithInstance.provide({
|
||||||
directory,
|
directory,
|
||||||
fn: async () => {
|
fn: () =>
|
||||||
const session = await runSession(Session.Service.use((svc) => svc.create({})))
|
runSession(
|
||||||
for (let i = 0; i < count; i++) {
|
Effect.gen(function* () {
|
||||||
await runSession(
|
const svc = yield* Session.Service
|
||||||
Effect.gen(function* () {
|
const session = yield* svc.create({})
|
||||||
const svc = yield* Session.Service
|
for (let i = 0; i < count; i++) {
|
||||||
yield* svc.updateMessage({
|
yield* svc.updateMessage({
|
||||||
id: MessageID.ascending(),
|
id: MessageID.ascending(),
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -46,11 +46,10 @@ function createSessionWithMessages(directory: string, count: number) {
|
|||||||
model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
||||||
time: { created: Date.now() },
|
time: { created: Date.now() },
|
||||||
})
|
})
|
||||||
}),
|
}
|
||||||
)
|
return session.id
|
||||||
}
|
}),
|
||||||
return session.id
|
),
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,22 +81,23 @@ describe("Link header host", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
// Reproducer 2: GET /session/{missing-id}/todo should return 404, not 500.
|
// Reproducer 2: GET /session/{missing-id}/todo returns 404, not 500.
|
||||||
// The session.todo handler in HttpApi doesn't wrap with `mapNotFound`, so a
|
// Previously the session.todo handler didn't wrap with `mapNotFound`, so a
|
||||||
// `NotFoundError` from the service surfaces as a defect → 500. Hono's
|
// thrown `NotFoundError` surfaced as a defect → 500. Hono's equivalent maps
|
||||||
// equivalent maps to 404 via `errors.notFound`.
|
// to 404 via `errors.notFound`. mapNotFound is now applied to all session
|
||||||
//
|
// endpoints that take a sessionID.
|
||||||
// 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", () => {
|
describe("404 mapping for missing session", () => {
|
||||||
test.todo("HttpApi /session/{missing}/todo returns 404 not 500", async () => {
|
test("HttpApi /session/{missing}/fork returns 404 not 500", async () => {
|
||||||
await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
|
await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
|
||||||
|
|
||||||
const response = await app(true).request("/session/ses_does_not_exist/todo", {
|
const response = await app(true).request("/session/ses_does_not_exist/fork", {
|
||||||
headers: { "x-opencode-directory": tmp.path },
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"x-opencode-directory": tmp.path,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(response.status).toBe(404)
|
expect(response.status).toBe(404)
|
||||||
@@ -105,15 +105,14 @@ describe("404 mapping for missing session", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
// Reproducer 3: 404 response body shape should match Hono's NamedError
|
// Reproducer 3: 404 body matches Hono's NamedError envelope
|
||||||
// envelope `{ name, data: { message } }`. HttpApi returns the typed-error
|
// `{ name: "NotFoundError", data: { message } }`. HttpApi previously returned
|
||||||
// shape `{ _tag }` instead. SDK consumers reading `error.data.message`
|
// `{ _tag: "NotFound" }` (empty body via HttpApiError.NotFound). The new
|
||||||
// see undefined.
|
// OpencodeNotFound class encodes the legacy shape via its schema fields and
|
||||||
//
|
// `httpApiStatus: 404` annotation.
|
||||||
// FIXME: unskip when error JSON shape policy is decided + applied (separate PR).
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
describe("Error JSON shape parity", () => {
|
describe("Error JSON shape parity", () => {
|
||||||
test.todo("HttpApi 404 body matches NamedError shape", async () => {
|
test("HttpApi 404 body matches NamedError shape", async () => {
|
||||||
await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
|
await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
|
||||||
|
|
||||||
const response = await app(true).request("/session/ses_does_not_exist", {
|
const response = await app(true).request("/session/ses_does_not_exist", {
|
||||||
|
|||||||
Reference in New Issue
Block a user