import { SessionV2 } from "@opencode-ai/core/session" import { DateTime, Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { Api } from "../api" import { SessionsCursor } from "../groups/session" import { ConflictError, InvalidCursorError, ServiceUnavailableError, SessionNotFoundError, UnknownError, } from "../errors" import { AbsolutePath } from "@opencode-ai/core/schema" const DefaultSessionsLimit = 50 export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) => Effect.gen(function* () { const session = yield* SessionV2.Service return handlers .handle( "session.list", Effect.fn(function* (ctx) { const query = ctx.query.cursor !== undefined ? yield* SessionsCursor.parse(ctx.query.cursor).pipe( Effect.mapError(() => new InvalidCursorError({ message: "Invalid cursor" })), ) : ctx.query const sessions = yield* session.list({ ...query, workspaceID: query.workspace, limit: ctx.query.limit ?? DefaultSessionsLimit, }) const first = sessions[0] const last = sessions.at(-1) return { data: sessions, cursor: { previous: first ? SessionsCursor.make({ ...query, anchor: { id: first.id, time: DateTime.toEpochMillis(first.time.created), direction: "previous", }, }) : undefined, next: last ? SessionsCursor.make({ ...query, anchor: { id: last.id, time: DateTime.toEpochMillis(last.time.created), direction: "next", }, }) : undefined, }, } }), ) .handle( "session.create", Effect.fn(function* (ctx) { return { data: yield* session.create({ id: ctx.payload.id, agent: ctx.payload.agent, model: ctx.payload.model, location: ctx.payload.location ?? { directory: AbsolutePath.make(process.cwd()) }, }), } }), ) .handle( "session.get", Effect.fn(function* (ctx) { return { data: yield* session.get(ctx.params.sessionID).pipe( Effect.catchTag( "Session.NotFoundError", (error) => new SessionNotFoundError({ sessionID: error.sessionID, message: `Session not found: ${error.sessionID}`, }), ), ), } }), ) .handle( "session.prompt", Effect.fn(function* (ctx) { return { data: yield* session .prompt({ sessionID: ctx.params.sessionID, id: ctx.payload.id, prompt: ctx.payload.prompt, delivery: ctx.payload.delivery, resume: ctx.payload.resume, }) .pipe( Effect.catchTag("Session.NotFoundError", (error) => Effect.fail( new SessionNotFoundError({ sessionID: error.sessionID, message: `Session not found: ${error.sessionID}`, }), ), ), Effect.catchTag("Session.PromptConflictError", (error) => Effect.fail( new ConflictError({ message: `Prompt message ID conflicts with an existing durable record: ${error.messageID}`, resource: error.messageID, }), ), ), ), } }), ) .handle( "session.compact", Effect.fn(function* (ctx) { yield* session.compact({ sessionID: ctx.params.sessionID }).pipe( Effect.catchTag("Session.NotFoundError", (error) => Effect.fail( new SessionNotFoundError({ sessionID: error.sessionID, message: `Session not found: ${error.sessionID}`, }), ), ), Effect.catchTag("Session.OperationUnavailableError", (error) => Effect.fail( new ServiceUnavailableError({ message: `Session ${error.operation} is not available yet`, service: `session.${error.operation}`, }), ), ), ) return HttpApiSchema.NoContent.make() }), ) .handle( "session.wait", Effect.fn(function* (ctx) { yield* session.wait(ctx.params.sessionID).pipe( Effect.catchTag("Session.NotFoundError", (error) => Effect.fail( new SessionNotFoundError({ sessionID: error.sessionID, message: `Session not found: ${error.sessionID}`, }), ), ), Effect.catchTag("Session.OperationUnavailableError", (error) => Effect.fail( new ServiceUnavailableError({ message: `Session ${error.operation} is not available yet`, service: `session.${error.operation}`, }), ), ), ) return HttpApiSchema.NoContent.make() }), ) .handle( "session.context", Effect.fn(function* (ctx) { return { data: yield* session.context(ctx.params.sessionID).pipe( Effect.catchTag("Session.NotFoundError", (error) => Effect.fail( new SessionNotFoundError({ sessionID: error.sessionID, message: `Session not found: ${error.sessionID}`, }), ), ), Effect.catchTag("Session.MessageDecodeError", (error) => { const ref = `err_${crypto.randomUUID().slice(0, 8)}` return Effect.logError("failed to decode session message").pipe( Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }), Effect.andThen( Effect.fail( new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref, }), ), ), ) }), ), } }), ) }), )