From 1a2b1837e864ce193e354f39638ee10acf101c02 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 1 May 2026 18:55:09 -0400 Subject: [PATCH] update httpapi routes --- packages/opencode/src/server/backend.ts | 2 +- .../routes/instance/httpapi/groups/sync.ts | 28 +++++++++++++ .../instance/httpapi/groups/workspace.ts | 16 +++++++- .../routes/instance/httpapi/handlers/sync.ts | 40 ++++++++++++++++++- .../instance/httpapi/handlers/workspace.ts | 21 +++++++++- 5 files changed, 101 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/server/backend.ts b/packages/opencode/src/server/backend.ts index 8c9986f094..f456dc0be5 100644 --- a/packages/opencode/src/server/backend.ts +++ b/packages/opencode/src/server/backend.ts @@ -11,7 +11,7 @@ export type Selection = { export type Attributes = ReturnType export function select(): Selection { - // if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) return { backend: "effect-httpapi", reason: "env" } + if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) return { backend: "effect-httpapi", reason: "env" } return { backend: "hono", reason: "stable" } } diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts index 58d30b4c78..cf3fafad55 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts @@ -1,4 +1,5 @@ import { NonNegativeInt } from "@/util/schema" +import { SessionID } from "@/session/schema" import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../middleware/authorization" @@ -21,6 +22,9 @@ export const ReplayPayload = Schema.Struct({ export const ReplayResponse = Schema.Struct({ sessionID: Schema.String, }) +export const SessionPayload = Schema.Struct({ + sessionID: SessionID, +}) export const HistoryPayload = Schema.Record(Schema.String, NonNegativeInt) export const HistoryEvent = Schema.Struct({ id: Schema.String, @@ -33,6 +37,8 @@ export const HistoryEvent = Schema.Struct({ export const SyncPaths = { start: `${root}/start`, replay: `${root}/replay`, + erase: `${root}/erase`, + steal: `${root}/steal`, history: `${root}/history`, } as const @@ -60,6 +66,28 @@ export const SyncApi = HttpApi.make("sync") description: "Validate and replay a complete sync event history.", }), ), + HttpApiEndpoint.post("erase", SyncPaths.erase, { + payload: SessionPayload, + success: described(SessionPayload, "Erased session sync events"), + error: HttpApiError.BadRequest, + }).annotateMerge( + OpenApi.annotations({ + identifier: "sync.erase", + summary: "Erase session sync events", + description: "Erase all locally stored sync events for a session aggregate.", + }), + ), + HttpApiEndpoint.post("steal", SyncPaths.steal, { + payload: SessionPayload, + success: described(SessionPayload, "Session stolen into workspace"), + error: HttpApiError.BadRequest, + }).annotateMerge( + OpenApi.annotations({ + identifier: "sync.steal", + summary: "Steal session into workspace", + description: "Update a session to belong to the current workspace through the sync event system.", + }), + ), HttpApiEndpoint.post("history", SyncPaths.history, { payload: HistoryPayload, success: described(Schema.Array(HistoryEvent), "Sync events"), diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts index aa79bc86f8..591d2b0ab2 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts @@ -1,7 +1,7 @@ import { Workspace } from "@/control-plane/workspace" import { WorkspaceAdaptorEntry } from "@/control-plane/types" import { Schema, Struct } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" @@ -9,12 +9,14 @@ import { described } from "./metadata" const root = "/experimental/workspace" export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"])) +export const WarpPayload = Schema.Struct(Struct.omit(Workspace.SessionWarpInput.fields, ["workspaceID"])) export const WorkspacePaths = { adaptors: `${root}/adaptor`, list: root, status: `${root}/status`, remove: `${root}/:id`, + warp: `${root}/:id/warp`, } as const export const WorkspaceApi = HttpApi.make("workspace") @@ -70,6 +72,18 @@ export const WorkspaceApi = HttpApi.make("workspace") description: "Remove an existing workspace.", }), ), + HttpApiEndpoint.post("warp", WorkspacePaths.warp, { + params: { id: Workspace.Info.fields.id }, + payload: WarpPayload, + success: described(HttpApiSchema.NoContent, "Session warped"), + error: HttpApiError.BadRequest, + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.workspace.warp", + summary: "Warp session into workspace", + description: "Move a session's sync history into the target workspace.", + }), + ), ) .annotateMerge(OpenApi.annotations({ title: "workspace", description: "Experimental HttpApi workspace routes." })) .middleware(InstanceContextMiddleware) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts index 3ae091484f..0beea29933 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts @@ -1,5 +1,8 @@ import { startWorkspaceSyncing } from "@/control-plane/workspace" +import { WorkspaceContext } from "@/control-plane/workspace-context" import * as InstanceState from "@/effect/instance-state" +import { Instance } from "@/project/instance" +import { Session } from "@/session/session" import { Database } from "@/storage/db" import { SyncEvent } from "@/sync" import { EventTable } from "@/sync/event.sql" @@ -12,7 +15,7 @@ import { or } from "drizzle-orm" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" -import { HistoryPayload, ReplayPayload } from "../groups/sync" +import { HistoryPayload, ReplayPayload, SessionPayload } from "../groups/sync" export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handlers) => Effect.gen(function* () { @@ -33,6 +36,34 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl return { sessionID: events[0].aggregateID } }) + const erase = Effect.fn("SyncHttpApi.erase")(function* (ctx: { payload: typeof SessionPayload.Type }) { + SyncEvent.remove(ctx.payload.sessionID) + return { sessionID: ctx.payload.sessionID } + }) + + const steal = Effect.fn("SyncHttpApi.steal")(function* (ctx: { payload: typeof SessionPayload.Type }) { + const instance = yield* InstanceState.context + const workspaceID = yield* InstanceState.workspaceID + if (!workspaceID) throw new Error("Cannot steal session without workspace context") + + yield* Effect.sync(() => + WorkspaceContext.provide({ + workspaceID, + fn: () => + Instance.restore(instance, () => + SyncEvent.run(Session.Event.Updated, { + sessionID: ctx.payload.sessionID, + info: { + workspaceID, + }, + }), + ), + }), + ) + + return { sessionID: ctx.payload.sessionID } + }) + const history = Effect.fn("SyncHttpApi.history")(function* (ctx: { payload: typeof HistoryPayload.Type }) { const exclude = Object.entries(ctx.payload) return Database.use((db) => @@ -49,6 +80,11 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl ) }) - return handlers.handle("start", start).handle("replay", replay).handle("history", history) + return handlers + .handle("start", start) + .handle("replay", replay) + .handle("erase", erase) + .handle("steal", steal) + .handle("history", history) }), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts index 45b80ef2e7..69999122cf 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts @@ -3,9 +3,9 @@ import { Workspace } from "@/control-plane/workspace" import * as InstanceState from "@/effect/instance-state" import { Instance } from "@/project/instance" import { Effect } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" +import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" -import { CreatePayload } from "../groups/workspace" +import { CreatePayload, WarpPayload } from "../groups/workspace" export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) => Effect.gen(function* () { @@ -40,11 +40,28 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac return yield* Effect.promise(() => Instance.restore(instance, () => Workspace.remove(ctx.params.id))) }) + const warp = Effect.fn("WorkspaceHttpApi.warp")(function* (ctx: { + params: { id: Workspace.Info["id"] } + payload: typeof WarpPayload.Type + }) { + const instance = yield* InstanceState.context + yield* Effect.promise(() => + Instance.restore(instance, () => + Workspace.sessionWarp({ + workspaceID: ctx.params.id, + ...ctx.payload, + }), + ), + ) + return HttpApiSchema.NoContent.make() + }) + return handlers .handle("adaptors", adaptors) .handle("list", list) .handle("create", create) .handle("status", status) .handle("remove", remove) + .handle("warp", warp) }), )