feat(core): session warping (#25768)
This commit is contained in:
@@ -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,7 @@ export const HistoryEvent = Schema.Struct({
|
||||
export const SyncPaths = {
|
||||
start: `${root}/start`,
|
||||
replay: `${root}/replay`,
|
||||
steal: `${root}/steal`,
|
||||
history: `${root}/history`,
|
||||
} as const
|
||||
|
||||
@@ -60,6 +65,17 @@ export const SyncApi = HttpApi.make("sync")
|
||||
description: "Validate and replay a complete sync event history.",
|
||||
}),
|
||||
),
|
||||
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"),
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { WorkspaceAdapterEntry } from "@/control-plane/types"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
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"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const root = "/experimental/workspace"
|
||||
export const CreatePayload = Schema.Struct({
|
||||
...Struct.omit(Workspace.CreateInput.fields, ["projectID", "extra"]),
|
||||
extra: Schema.optional(Workspace.CreateInput.fields.extra),
|
||||
})
|
||||
export const SessionRestorePayload = Schema.Struct(Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]))
|
||||
export const SessionRestoreResponse = Schema.Struct({
|
||||
total: NonNegativeInt,
|
||||
export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"]))
|
||||
export const WarpPayload = Schema.Struct({
|
||||
id: Schema.NullOr(Workspace.Info.fields.id),
|
||||
sessionID: Workspace.SessionWarpInput.fields.sessionID,
|
||||
})
|
||||
|
||||
export const WorkspacePaths = {
|
||||
@@ -23,7 +19,7 @@ export const WorkspacePaths = {
|
||||
list: root,
|
||||
status: `${root}/status`,
|
||||
remove: `${root}/:id`,
|
||||
sessionRestore: `${root}/:id/session-restore`,
|
||||
warp: `${root}/warp`,
|
||||
} as const
|
||||
|
||||
export const WorkspaceApi = HttpApi.make("workspace")
|
||||
@@ -79,16 +75,15 @@ export const WorkspaceApi = HttpApi.make("workspace")
|
||||
description: "Remove an existing workspace.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("sessionRestore", WorkspacePaths.sessionRestore, {
|
||||
params: { id: Workspace.Info.fields.id },
|
||||
payload: SessionRestorePayload,
|
||||
success: described(SessionRestoreResponse, "Session replay started"),
|
||||
HttpApiEndpoint.post("warp", WorkspacePaths.warp, {
|
||||
payload: WarpPayload,
|
||||
success: described(HttpApiSchema.NoContent, "Session warped"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.sessionRestore",
|
||||
summary: "Restore session into workspace",
|
||||
description: "Replay a session's sync events into the target workspace in batches.",
|
||||
identifier: "experimental.workspace.warp",
|
||||
summary: "Warp session into workspace",
|
||||
description: "Move a session's sync history into the target workspace, or detach it to the local project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { Session } from "@/session/session"
|
||||
import { Database } from "@/storage/db"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { EventTable } from "@/sync/event.sql"
|
||||
@@ -12,7 +13,7 @@ import { or } from "drizzle-orm"
|
||||
import { Effect, Scope } 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"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
|
||||
const log = Log.create({ service: "server.sync" })
|
||||
@@ -56,6 +57,25 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
|
||||
return { sessionID: source }
|
||||
})
|
||||
|
||||
const steal = Effect.fn("SyncHttpApi.steal")(function* (ctx: { payload: typeof SessionPayload.Type }) {
|
||||
const workspaceID = yield* InstanceState.workspaceID
|
||||
if (!workspaceID) throw new Error("Cannot steal session without workspace context")
|
||||
|
||||
yield* sync.run(Session.Event.Updated, {
|
||||
sessionID: ctx.payload.sessionID,
|
||||
info: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
|
||||
log.info("sync session stolen", {
|
||||
sessionID: ctx.payload.sessionID,
|
||||
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) =>
|
||||
@@ -72,6 +92,6 @@ 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("steal", steal).handle("history", history)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as InstanceState from "@/effect/instance-state"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
import { CreatePayload, SessionRestorePayload } from "../groups/workspace"
|
||||
import { CreatePayload, WarpPayload } from "../groups/workspace"
|
||||
|
||||
export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -39,13 +39,10 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
|
||||
return yield* workspace.remove(ctx.params.id)
|
||||
})
|
||||
|
||||
const sessionRestore = Effect.fn("WorkspaceHttpApi.sessionRestore")(function* (ctx: {
|
||||
params: { id: Workspace.Info["id"] }
|
||||
payload: typeof SessionRestorePayload.Type
|
||||
}) {
|
||||
return yield* workspace
|
||||
.sessionRestore({
|
||||
workspaceID: ctx.params.id,
|
||||
const warp = Effect.fn("WorkspaceHttpApi.warp")(function* (ctx: { payload: typeof WarpPayload.Type }) {
|
||||
yield* workspace
|
||||
.sessionWarp({
|
||||
workspaceID: ctx.payload.id,
|
||||
sessionID: ctx.payload.sessionID,
|
||||
})
|
||||
.pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
|
||||
@@ -57,6 +54,6 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
|
||||
.handle("create", create)
|
||||
.handle("status", status)
|
||||
.handle("remove", remove)
|
||||
.handle("sessionRestore", sessionRestore)
|
||||
.handle("warp", warp)
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user