refactor(core): move database schema ownership (#29068)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Schema } from "effect"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
||||
export const Event = {
|
||||
Connected: BusEvent.define("server.connected", Schema.Struct({})),
|
||||
Disposed: BusEvent.define("global.disposed", Schema.Struct({})),
|
||||
Connected: EventV2.define({ type: "server.connected", schema: {} }),
|
||||
Disposed: EventV2.define({ type: "global.disposed", schema: {} }),
|
||||
}
|
||||
|
||||
@@ -1,26 +1,2 @@
|
||||
import sessionProjectors from "../session/projectors"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionTable } from "@/session/session.sql"
|
||||
import { Database } from "@/storage/db"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
export function initProjectors() {
|
||||
SyncEvent.init({
|
||||
projectors: sessionProjectors,
|
||||
convertEvent: (type, data) => {
|
||||
if (type === "session.updated") {
|
||||
const id = (data as SyncEvent.Event<typeof Session.Event.Updated>["data"]).sessionID
|
||||
const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
|
||||
if (!row) return data
|
||||
|
||||
return {
|
||||
sessionID: id,
|
||||
info: Session.fromRow(row),
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi } from "effect/unstable/httpapi"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { ConfigApi } from "./groups/config"
|
||||
import { ControlApi } from "./groups/control"
|
||||
import { EventApi } from "./groups/event"
|
||||
@@ -23,9 +22,18 @@ import { V2Api } from "./groups/v2"
|
||||
import { Authorization } from "./middleware/authorization"
|
||||
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
||||
|
||||
// SSE event schemas built from the BusEvent/SyncEvent registries.
|
||||
const EventSchema = Schema.Union(BusEvent.effectPayloads()).annotate({ identifier: "Event" })
|
||||
const SyncEventSchemas = SyncEvent.effectPayloads()
|
||||
const EventSchema = Schema.Union(
|
||||
EventV2.registry
|
||||
.values()
|
||||
.map((definition) =>
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
type: Schema.Literal(definition.type),
|
||||
properties: definition.data,
|
||||
}).annotate({ identifier: `Event.${definition.type}` }),
|
||||
)
|
||||
.toArray(),
|
||||
).annotate({ identifier: "Event" })
|
||||
|
||||
export const RootHttpApi = HttpApi.make("opencode-root")
|
||||
.addHttpApi(ControlApi)
|
||||
@@ -56,7 +64,7 @@ export const OpenCodeHttpApi = HttpApi.make("opencode")
|
||||
.addHttpApi(EventApi)
|
||||
.addHttpApi(InstanceHttpApi)
|
||||
.addHttpApi(PtyConnectApi)
|
||||
.annotate(HttpApi.AdditionalSchemas, [EventSchema, ...SyncEventSchemas])
|
||||
.annotate(HttpApi.AdditionalSchemas, [EventSchema])
|
||||
|
||||
export type RootHttpApiType = typeof RootHttpApi
|
||||
export type InstanceHttpApiType = typeof InstanceHttpApi
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Auth } from "@/auth"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { described } from "./metadata"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const AuthParams = Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
providerID: ProviderV2.ID,
|
||||
})
|
||||
|
||||
const LogQuery = Schema.Struct({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AccountID, OrgID } from "@/account/schema"
|
||||
import { MCP } from "@/mcp"
|
||||
import { ProviderID, ModelID } from "@/provider/schema"
|
||||
|
||||
import { Session } from "@/session/session"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
import { QueryBoolean } from "./query"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const ConsoleStateResponse = Schema.Struct({
|
||||
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
||||
@@ -49,8 +50,8 @@ const ToolListItem = Schema.Struct({
|
||||
const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
|
||||
export const ToolListQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
provider: ProviderID,
|
||||
model: ModelID,
|
||||
provider: ProviderV2.ID,
|
||||
model: ProviderV2.ModelID,
|
||||
})
|
||||
|
||||
const WorktreeList = Schema.Array(Schema.String)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import "@/server/event"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
@@ -15,7 +14,14 @@ const GlobalEventSchema = Schema.Struct({
|
||||
directory: Schema.String,
|
||||
project: Schema.optional(Schema.String),
|
||||
workspace: Schema.optional(Schema.String),
|
||||
payload: Schema.Union([...BusEvent.effectPayloads(), ...SyncEvent.effectPayloads()]),
|
||||
payload: Schema.Union(
|
||||
EventV2.registry
|
||||
.values()
|
||||
.map((definition) =>
|
||||
Schema.Struct({ id: Schema.String, type: Schema.Literal(definition.type), properties: definition.data }),
|
||||
)
|
||||
.toArray(),
|
||||
),
|
||||
}).annotate({ identifier: "GlobalEvent" })
|
||||
|
||||
export const GlobalUpgradeInput = Schema.Struct({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Project } from "@/project/project"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ProjectNotFoundError } from "../errors"
|
||||
@@ -50,7 +50,7 @@ export const ProjectApi = HttpApi.make("project")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("update", `${root}/:projectID`, {
|
||||
params: { projectID: ProjectID },
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: UpdatePayload,
|
||||
success: described(Project.Info, "Updated project information"),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../middleware/authorization"
|
||||
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
||||
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
|
||||
import { described } from "./metadata"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const root = "/provider"
|
||||
|
||||
@@ -21,7 +22,7 @@ export class ProviderAuthApiError extends Schema.ErrorClass<ProviderAuthApiError
|
||||
{
|
||||
name: ProviderAuthErrorName,
|
||||
data: Schema.Struct({
|
||||
providerID: Schema.optional(ProviderID),
|
||||
providerID: Schema.optional(ProviderV2.ID),
|
||||
field: Schema.optional(Schema.String),
|
||||
message: Schema.optional(Schema.String),
|
||||
kind: Schema.optional(Schema.String),
|
||||
@@ -55,7 +56,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
|
||||
params: { providerID: ProviderID },
|
||||
params: { providerID: ProviderV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ProviderAuth.AuthorizeInput,
|
||||
success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"),
|
||||
@@ -68,7 +69,7 @@ export const ProviderApi = HttpApi.make("provider")
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
|
||||
params: { providerID: ProviderID },
|
||||
params: { providerID: ProviderV2.ID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ProviderAuth.CallbackInput,
|
||||
success: described(Schema.Boolean, "OAuth callback processed successfully"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Permission } from "@/permission"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { SessionPrompt } from "@/session/prompt"
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../errors"
|
||||
import { described } from "./metadata"
|
||||
import { QueryBoolean } from "./query"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const root = "/session"
|
||||
export const ListQuery = Schema.Struct({
|
||||
@@ -55,13 +57,13 @@ export const UpdatePayload = Schema.Struct({
|
||||
})
|
||||
export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"]))
|
||||
export const InitPayload = Schema.Struct({
|
||||
modelID: ModelID,
|
||||
providerID: ProviderID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
providerID: ProviderV2.ID,
|
||||
messageID: MessageID,
|
||||
})
|
||||
export const SummarizePayload = Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
auto: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"]))
|
||||
@@ -176,7 +178,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.get("messages", SessionPaths.messages, {
|
||||
params: { sessionID: SessionID },
|
||||
query: MessagesQuery,
|
||||
success: described(Schema.Array(MessageV2.WithParts), "List of messages"),
|
||||
success: described(Schema.Array(SessionLegacy.WithParts), "List of messages"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -188,7 +190,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.get("message", SessionPaths.message, {
|
||||
params: { sessionID: SessionID, messageID: MessageID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(MessageV2.WithParts, "Message"),
|
||||
success: described(SessionLegacy.WithParts, "Message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -314,7 +316,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: PromptPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
success: described(SessionLegacy.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -341,7 +343,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: CommandPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
success: described(SessionLegacy.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -354,7 +356,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ShellPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
success: described(SessionLegacy.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -430,8 +432,8 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
|
||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: MessageV2.Part,
|
||||
success: described(MessageV2.Part, "Successfully updated part"),
|
||||
payload: SessionLegacy.Part,
|
||||
success: described(SessionLegacy.Part, "Successfully updated part"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -12,19 +12,19 @@ const root = "/tui"
|
||||
export const CommandPayload = Schema.Struct({ command: Schema.String })
|
||||
const EventTuiPromptAppend = Schema.Struct({
|
||||
type: Schema.Literal(TuiEvent.PromptAppend.type),
|
||||
properties: TuiEvent.PromptAppend.properties,
|
||||
properties: TuiEvent.PromptAppend.data,
|
||||
}).annotate({ identifier: "EventTuiPromptAppend" })
|
||||
const EventTuiCommandExecute = Schema.Struct({
|
||||
type: Schema.Literal(TuiEvent.CommandExecute.type),
|
||||
properties: TuiEvent.CommandExecute.properties,
|
||||
properties: TuiEvent.CommandExecute.data,
|
||||
}).annotate({ identifier: "EventTuiCommandExecute" })
|
||||
const EventTuiToastShow = Schema.Struct({
|
||||
type: Schema.Literal(TuiEvent.ToastShow.type),
|
||||
properties: TuiEvent.ToastShow.properties,
|
||||
properties: TuiEvent.ToastShow.data,
|
||||
}).annotate({ identifier: "EventTuiToastShow" })
|
||||
const EventTuiSessionSelect = Schema.Struct({
|
||||
type: Schema.Literal(TuiEvent.SessionSelect.type),
|
||||
properties: TuiEvent.SessionSelect.properties,
|
||||
properties: TuiEvent.SessionSelect.data,
|
||||
}).annotate({ identifier: "EventTuiSessionSelect" })
|
||||
export const TuiPublishPayload = Schema.Union([
|
||||
EventTuiPromptAppend,
|
||||
@@ -55,7 +55,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
.add(
|
||||
HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: TuiEvent.PromptAppend.properties,
|
||||
payload: TuiEvent.PromptAppend.data,
|
||||
success: described(Schema.Boolean, "Prompt processed successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
@@ -139,7 +139,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
),
|
||||
HttpApiEndpoint.post("showToast", TuiPaths.showToast, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: TuiEvent.ToastShow.properties,
|
||||
payload: TuiEvent.ToastShow.data,
|
||||
success: described(Schema.Boolean, "Toast notification shown successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -162,7 +162,7 @@ export const TuiApi = HttpApi.make("tui")
|
||||
),
|
||||
HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, {
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: TuiEvent.SessionSelect.properties,
|
||||
payload: TuiEvent.SessionSelect.data,
|
||||
success: described(Schema.Boolean, "Session selected successfully"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import { Prompt } from "@opencode-ai/core/session-prompt"
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import {
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Auth } from "@/auth"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { RootHttpApi } from "../api"
|
||||
import { LogInput } from "../groups/control"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const auth = yield* Auth.Service
|
||||
|
||||
const authSet = Effect.fn("ControlHttpApi.authSet")(function* (ctx: {
|
||||
params: { providerID: ProviderID }
|
||||
params: { providerID: ProviderV2.ID }
|
||||
payload: Auth.Info
|
||||
}) {
|
||||
yield* auth.set(ctx.params.providerID, ctx.payload).pipe(Effect.orDie)
|
||||
return true
|
||||
})
|
||||
|
||||
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderID } }) {
|
||||
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderV2.ID } }) {
|
||||
yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Queue } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
@@ -18,24 +21,51 @@ function eventData(data: unknown): Sse.Event {
|
||||
}
|
||||
}
|
||||
|
||||
function eventResponse(bus: Bus.Interface) {
|
||||
function eventID() {
|
||||
return EventV2.ID.create()
|
||||
}
|
||||
|
||||
function eventResponse(events: EventV2.Interface) {
|
||||
return Effect.gen(function* () {
|
||||
// Subscribe eagerly: the bus subscription is acquired in the request scope
|
||||
// at this yield, so any publish from now on is queued for the body-pump
|
||||
// fiber to drain — closing the race where Stream.concat(server.connected,
|
||||
// lazy-subscribe) used to drop publishes in the prefix-consume window.
|
||||
const events = (yield* bus.subscribeAll()).pipe(
|
||||
Stream.takeUntil((event) => event.type === Bus.InstanceDisposed.type),
|
||||
const instance = yield* InstanceState.context
|
||||
const workspaceID = yield* InstanceState.workspaceID
|
||||
// Listener registration is eager, so events published after this point cannot
|
||||
// be lost while the HTTP body fiber is starting or emitting server.connected.
|
||||
const queue = yield* Queue.unbounded<EventV2.Payload>()
|
||||
const unsubscribe = yield* events.listen((event) => Effect.sync(() => Queue.offerUnsafe(queue, event)))
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
const stream = Stream.fromQueue(queue).pipe(
|
||||
Stream.filter(
|
||||
(event) =>
|
||||
event.location?.directory === instance.directory &&
|
||||
(event.location.workspaceID === undefined || event.location.workspaceID === workspaceID),
|
||||
),
|
||||
Stream.map((event) => ({ id: event.id, type: event.type, properties: event.data })),
|
||||
)
|
||||
const disposed = Stream.callback<{ id: string; type: string; properties: unknown }>((queue) => {
|
||||
const listener = (event: { directory?: string; payload: { id?: string; type?: string; properties?: unknown } }) => {
|
||||
if (event.directory !== instance.directory || event.payload.type !== "server.instance.disposed") return
|
||||
Queue.offerUnsafe(queue, {
|
||||
id: event.payload.id ?? eventID(),
|
||||
type: "server.instance.disposed",
|
||||
properties: event.payload.properties ?? {},
|
||||
})
|
||||
}
|
||||
return Effect.acquireRelease(
|
||||
Effect.sync(() => GlobalBus.on("event", listener)),
|
||||
() => Effect.sync(() => GlobalBus.off("event", listener)),
|
||||
)
|
||||
})
|
||||
const output = stream.pipe(Stream.merge(disposed, { haltStrategy: "left" }), Stream.takeUntil((event) => event.type === "server.instance.disposed"))
|
||||
const heartbeat = Stream.tick("10 seconds").pipe(
|
||||
Stream.drop(1),
|
||||
Stream.map(() => ({ id: Bus.createID(), type: "server.heartbeat", properties: {} })),
|
||||
Stream.map(() => ({ id: eventID(), type: "server.heartbeat", properties: {} })),
|
||||
)
|
||||
|
||||
log.info("event connected")
|
||||
return HttpServerResponse.stream(
|
||||
Stream.make({ id: Bus.createID(), type: "server.connected", properties: {} }).pipe(
|
||||
Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
|
||||
Stream.make({ id: eventID(), type: "server.connected", properties: {} }).pipe(
|
||||
Stream.concat(output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
|
||||
Stream.map(eventData),
|
||||
Stream.pipeThroughChannel(Sse.encode()),
|
||||
Stream.encodeText,
|
||||
@@ -55,11 +85,11 @@ function eventResponse(bus: Bus.Interface) {
|
||||
|
||||
export const eventHandlers = HttpApiBuilder.group(EventApi, "event", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
return handlers.handleRaw(
|
||||
"subscribe",
|
||||
Effect.fn("EventHttpApi.subscribe")(function* () {
|
||||
return yield* eventResponse(bus)
|
||||
return yield* eventResponse(events)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -29,6 +29,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
|
||||
const project = yield* Project.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const worktreeSvc = yield* Worktree.Service
|
||||
const sessions = yield* Session.Service
|
||||
|
||||
const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
|
||||
const [state, groups] = yield* Effect.all(
|
||||
@@ -127,21 +128,19 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
|
||||
|
||||
const session = Effect.fn("ExperimentalHttpApi.session")(function* (ctx: { query: typeof SessionListQuery.Type }) {
|
||||
const limit = ctx.query.limit ?? 100
|
||||
const sessions = Array.from(
|
||||
Session.listGlobal({
|
||||
directory: ctx.query.directory,
|
||||
roots: ctx.query.roots,
|
||||
start: ctx.query.start,
|
||||
cursor: ctx.query.cursor,
|
||||
search: ctx.query.search,
|
||||
limit: limit + 1,
|
||||
archived: ctx.query.archived,
|
||||
}),
|
||||
)
|
||||
const list = sessions.length > limit ? sessions.slice(0, limit) : sessions
|
||||
const all = yield* sessions.listGlobal({
|
||||
directory: ctx.query.directory,
|
||||
roots: ctx.query.roots,
|
||||
start: ctx.query.start,
|
||||
cursor: ctx.query.cursor,
|
||||
search: ctx.query.search,
|
||||
limit: limit + 1,
|
||||
archived: ctx.query.archived,
|
||||
})
|
||||
const list = all.length > limit ? all.slice(0, limit) : all
|
||||
return HttpServerResponse.jsonUnsafe(list, {
|
||||
headers:
|
||||
sessions.length > limit && list.length > 0
|
||||
all.length > limit && list.length > 0
|
||||
? { "x-next-cursor": String(list[list.length - 1].time.updated) }
|
||||
: undefined,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { GlobalBus, type GlobalEvent as GlobalBusEvent } from "@/bus/global"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { Bus } from "@/bus"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Installation } from "@/installation"
|
||||
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
@@ -44,11 +44,11 @@ function eventResponse() {
|
||||
})
|
||||
const heartbeat = Stream.tick("10 seconds").pipe(
|
||||
Stream.drop(1),
|
||||
Stream.map(() => ({ payload: { id: Bus.createID(), type: "server.heartbeat", properties: {} } })),
|
||||
Stream.map(() => ({ payload: { id: EventV2.ID.create(), type: "server.heartbeat", properties: {} } })),
|
||||
)
|
||||
|
||||
return HttpServerResponse.stream(
|
||||
Stream.make({ payload: { id: Bus.createID(), type: "server.connected", properties: {} } }).pipe(
|
||||
Stream.make({ payload: { id: EventV2.ID.create(), type: "server.connected", properties: {} } }).pipe(
|
||||
Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
|
||||
Stream.map(eventData),
|
||||
Stream.pipeThroughChannel(Sse.encode()),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as InstanceState from "@/effect/instance-state"
|
||||
import { Project } from "@/project/project"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
@@ -33,7 +33,7 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project",
|
||||
})
|
||||
|
||||
const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: {
|
||||
params: { projectID: ProjectID }
|
||||
params: { projectID: ProjectV2.ID }
|
||||
payload: Project.UpdatePayload
|
||||
}) {
|
||||
return yield* svc.update({ ...ctx.payload, projectID: ctx.params.projectID }).pipe(
|
||||
|
||||
@@ -2,13 +2,14 @@ import { ProviderAuth } from "@/provider/auth"
|
||||
import { Config } from "@/config/config"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
|
||||
import { mapValues } from "remeda"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../api"
|
||||
import { ProviderAuthApiError } from "../groups/provider"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
function mapProviderAuthError<A, R>(self: Effect.Effect<A, ProviderAuth.Error, R>) {
|
||||
return self.pipe(
|
||||
@@ -62,7 +63,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider"
|
||||
})
|
||||
|
||||
const authorize = Effect.fn("ProviderHttpApi.authorize")(function* (ctx: {
|
||||
params: { providerID: ProviderID }
|
||||
params: { providerID: ProviderV2.ID }
|
||||
payload: ProviderAuth.AuthorizeInput
|
||||
}) {
|
||||
return yield* mapProviderAuthError(
|
||||
@@ -75,7 +76,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider"
|
||||
})
|
||||
|
||||
const authorizeRaw = Effect.fn("ProviderHttpApi.authorizeRaw")(function* (ctx: {
|
||||
params: { providerID: ProviderID }
|
||||
params: { providerID: ProviderV2.ID }
|
||||
request: HttpServerRequest.HttpServerRequest
|
||||
}) {
|
||||
const body = yield* Effect.orDie(ctx.request.text)
|
||||
@@ -90,7 +91,7 @@ export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider"
|
||||
})
|
||||
|
||||
const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: {
|
||||
params: { providerID: ProviderID }
|
||||
params: { providerID: ProviderV2.ID }
|
||||
payload: ProviderAuth.CallbackInput
|
||||
}) {
|
||||
yield* mapProviderAuthError(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { Bus } from "@/bus"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Command } from "@/command"
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
@@ -56,7 +57,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
const statusSvc = yield* SessionStatus.Service
|
||||
const todoSvc = yield* Todo.Service
|
||||
const summary = yield* SessionSummary.Service
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const scope = yield* Scope.Scope
|
||||
|
||||
const list = Effect.fn("SessionHttpApi.list")(function* (ctx: { query: typeof ListQuery.Type }) {
|
||||
@@ -316,7 +317,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
yield* Effect.logError("prompt_async failed").pipe(
|
||||
Effect.annotateLogs({ sessionID: ctx.params.sessionID, cause }),
|
||||
)
|
||||
yield* bus.publish(Session.Event.Error, {
|
||||
yield* events.publish(Session.Event.Error, {
|
||||
sessionID: ctx.params.sessionID,
|
||||
error: new NamedError.Unknown({ message: Cause.pretty(cause) }).toObject(),
|
||||
})
|
||||
@@ -395,10 +396,10 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
|
||||
const updatePart = Effect.fn("SessionHttpApi.updatePart")(function* (ctx: {
|
||||
params: { sessionID: SessionID; messageID: MessageID; partID: PartID }
|
||||
payload: typeof MessageV2.Part.Type
|
||||
payload: typeof SessionLegacy.Part.Type
|
||||
}) {
|
||||
yield* requireSession(ctx.params.sessionID)
|
||||
const payload = ctx.payload as MessageV2.Part
|
||||
const payload = ctx.payload as SessionLegacy.Part
|
||||
if (
|
||||
payload.id !== ctx.params.partID ||
|
||||
payload.messageID !== ctx.params.messageID ||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { EventTable } from "@opencode-ai/core/event/sql"
|
||||
import { asc } from "drizzle-orm"
|
||||
import { and } from "drizzle-orm"
|
||||
import { eq } from "drizzle-orm"
|
||||
@@ -21,8 +22,10 @@ const log = Log.create({ service: "server.sync" })
|
||||
export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
const session = yield* Session.Service
|
||||
const scope = yield* Scope.Scope
|
||||
const sync = yield* SyncEvent.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
const start = Effect.fn("SyncHttpApi.start")(function* () {
|
||||
yield* workspace
|
||||
@@ -32,27 +35,27 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
|
||||
})
|
||||
|
||||
const replay = Effect.fn("SyncHttpApi.replay")(function* (ctx: { payload: typeof ReplayPayload.Type }) {
|
||||
const events: SyncEvent.SerializedEvent[] = ctx.payload.events.map((event) => ({
|
||||
id: event.id,
|
||||
const payload: EventV2.SerializedEvent[] = ctx.payload.events.map((event) => ({
|
||||
id: EventV2.ID.make(event.id),
|
||||
aggregateID: event.aggregateID,
|
||||
seq: event.seq,
|
||||
type: event.type,
|
||||
data: { ...event.data },
|
||||
}))
|
||||
const source = events[0].aggregateID
|
||||
const source = payload[0].aggregateID
|
||||
log.info("sync replay requested", {
|
||||
sessionID: source,
|
||||
events: events.length,
|
||||
first: events[0]?.seq,
|
||||
last: events.at(-1)?.seq,
|
||||
events: payload.length,
|
||||
first: payload[0]?.seq,
|
||||
last: payload.at(-1)?.seq,
|
||||
directory: ctx.payload.directory,
|
||||
})
|
||||
yield* sync.replayAll(events)
|
||||
yield* events.replayAll(payload)
|
||||
log.info("sync replay complete", {
|
||||
sessionID: source,
|
||||
events: events.length,
|
||||
first: events[0]?.seq,
|
||||
last: events.at(-1)?.seq,
|
||||
events: payload.length,
|
||||
first: payload[0]?.seq,
|
||||
last: payload.at(-1)?.seq,
|
||||
})
|
||||
return { sessionID: source }
|
||||
})
|
||||
@@ -61,12 +64,7 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
|
||||
const workspaceID = yield* InstanceState.workspaceID
|
||||
if (!workspaceID) return yield* new HttpApiError.BadRequest({})
|
||||
|
||||
yield* sync.run(Session.Event.Updated, {
|
||||
sessionID: ctx.payload.sessionID,
|
||||
info: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
yield* session.setWorkspace({ sessionID: ctx.payload.sessionID, workspaceID })
|
||||
|
||||
log.info("sync session stolen", {
|
||||
sessionID: ctx.payload.sessionID,
|
||||
@@ -78,18 +76,17 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
|
||||
|
||||
const history = Effect.fn("SyncHttpApi.history")(function* (ctx: { payload: typeof HistoryPayload.Type }) {
|
||||
const exclude = Object.entries(ctx.payload)
|
||||
return Database.use((db) =>
|
||||
db
|
||||
.select()
|
||||
.from(EventTable)
|
||||
.where(
|
||||
exclude.length > 0
|
||||
? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!)
|
||||
: undefined,
|
||||
)
|
||||
.orderBy(asc(EventTable.seq))
|
||||
.all(),
|
||||
)
|
||||
return yield* db
|
||||
.select()
|
||||
.from(EventTable)
|
||||
.where(
|
||||
exclude.length > 0
|
||||
? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!)
|
||||
: undefined,
|
||||
)
|
||||
.orderBy(asc(EventTable.seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
return handlers.handle("start", start).handle("replay", replay).handle("steal", steal).handle("history", history)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bus } from "@/bus"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||
import { Session } from "@/session/session"
|
||||
import { Effect } from "effect"
|
||||
@@ -26,15 +26,15 @@ const commandAliases = {
|
||||
|
||||
export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const session = yield* Session.Service
|
||||
const publishCommand = (command: typeof TuiEvent.CommandExecute.properties.Type.command | undefined) =>
|
||||
bus.publish(TuiEvent.CommandExecute, { command } as typeof TuiEvent.CommandExecute.properties.Type)
|
||||
const publishCommand = (command: typeof TuiEvent.CommandExecute.data.Type.command | undefined) =>
|
||||
events.publish(TuiEvent.CommandExecute, { command } as typeof TuiEvent.CommandExecute.data.Type)
|
||||
|
||||
const appendPrompt = Effect.fn("TuiHttpApi.appendPrompt")(function* (ctx: {
|
||||
payload: typeof TuiEvent.PromptAppend.properties.Type
|
||||
payload: typeof TuiEvent.PromptAppend.data.Type
|
||||
}) {
|
||||
yield* bus.publish(TuiEvent.PromptAppend, ctx.payload)
|
||||
yield* events.publish(TuiEvent.PromptAppend, ctx.payload)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -77,29 +77,29 @@ export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handler
|
||||
})
|
||||
|
||||
const showToast = Effect.fn("TuiHttpApi.showToast")(function* (ctx: {
|
||||
payload: typeof TuiEvent.ToastShow.properties.Type
|
||||
payload: typeof TuiEvent.ToastShow.data.Type
|
||||
}) {
|
||||
yield* bus.publish(TuiEvent.ToastShow, ctx.payload)
|
||||
yield* events.publish(TuiEvent.ToastShow, ctx.payload)
|
||||
return true
|
||||
})
|
||||
|
||||
const publish = Effect.fn("TuiHttpApi.publish")(function* (ctx: { payload: typeof TuiPublishPayload.Type }) {
|
||||
if (ctx.payload.type === TuiEvent.PromptAppend.type)
|
||||
yield* bus.publish(TuiEvent.PromptAppend, ctx.payload.properties)
|
||||
yield* events.publish(TuiEvent.PromptAppend, ctx.payload.properties)
|
||||
if (ctx.payload.type === TuiEvent.CommandExecute.type)
|
||||
yield* bus.publish(TuiEvent.CommandExecute, ctx.payload.properties)
|
||||
if (ctx.payload.type === TuiEvent.ToastShow.type) yield* bus.publish(TuiEvent.ToastShow, ctx.payload.properties)
|
||||
yield* events.publish(TuiEvent.CommandExecute, ctx.payload.properties)
|
||||
if (ctx.payload.type === TuiEvent.ToastShow.type) yield* events.publish(TuiEvent.ToastShow, ctx.payload.properties)
|
||||
if (ctx.payload.type === TuiEvent.SessionSelect.type)
|
||||
yield* bus.publish(TuiEvent.SessionSelect, ctx.payload.properties)
|
||||
yield* events.publish(TuiEvent.SessionSelect, ctx.payload.properties)
|
||||
return true
|
||||
})
|
||||
|
||||
const selectSession = Effect.fn("TuiHttpApi.selectSession")(function* (ctx: {
|
||||
payload: typeof TuiEvent.SessionSelect.properties.Type
|
||||
payload: typeof TuiEvent.SessionSelect.data.Type
|
||||
}) {
|
||||
if (!ctx.payload.sessionID.startsWith("ses")) return yield* new HttpApiError.BadRequest({})
|
||||
yield* SessionError.mapStorageNotFound(session.get(ctx.payload.sessionID))
|
||||
yield* bus.publish(TuiEvent.SessionSelect, ctx.payload)
|
||||
yield* events.publish(TuiEvent.SessionSelect, ctx.payload)
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Layer } from "effect"
|
||||
import { layer as v2LocationLayer } from "../groups/v2/location"
|
||||
import { messageHandlers } from "./v2/message"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SessionMessage } from "@opencode-ai/core/session-message"
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { DateTime, Effect, Option, Schema } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../../api"
|
||||
@@ -20,7 +21,7 @@ const SessionCursor = Schema.Struct({
|
||||
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
|
||||
directory: Schema.String.pipe(Schema.optional),
|
||||
path: Schema.String.pipe(Schema.optional),
|
||||
workspaceID: WorkspaceID.pipe(Schema.optional),
|
||||
workspaceID: WorkspaceV2.ID.pipe(Schema.optional),
|
||||
roots: Schema.Boolean.pipe(Schema.optional),
|
||||
start: Schema.Finite.pipe(Schema.optional),
|
||||
search: Schema.String.pipe(Schema.optional),
|
||||
@@ -78,7 +79,7 @@ const sessionCursor = {
|
||||
|
||||
function decodeWorkspaceID(input: string | undefined) {
|
||||
if (input === undefined) return Effect.succeed(undefined)
|
||||
const workspaceID = Schema.decodeUnknownOption(WorkspaceID)(input)
|
||||
const workspaceID = Schema.decodeUnknownOption(WorkspaceV2.ID)(input)
|
||||
if (Option.isSome(workspaceID)) return Effect.succeed(workspaceID.value)
|
||||
return Effect.fail(
|
||||
new InvalidRequestError({
|
||||
@@ -114,17 +115,21 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
||||
start: ctx.query.start,
|
||||
search: ctx.query.search,
|
||||
}
|
||||
const sessions = yield* session.list({
|
||||
const input = {
|
||||
limit: ctx.query.limit ?? DefaultSessionsLimit,
|
||||
order,
|
||||
directory: filters.directory,
|
||||
path: filters.path,
|
||||
workspaceID: filters.workspaceID,
|
||||
roots: filters.roots,
|
||||
start: filters.start,
|
||||
search: filters.search,
|
||||
cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined,
|
||||
})
|
||||
}
|
||||
const sessions = yield* session.list(
|
||||
filters.directory
|
||||
? {
|
||||
...input,
|
||||
directory: AbsolutePath.make(filters.directory),
|
||||
}
|
||||
: input,
|
||||
)
|
||||
const first = sessions[0]
|
||||
const last = sessions.at(-1)
|
||||
return {
|
||||
@@ -168,7 +173,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
|
||||
.handle(
|
||||
"compact",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.compact(ctx.params.sessionID).pipe(
|
||||
yield* session.compact({ sessionID: ctx.params.sessionID }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Effect } from "effect"
|
||||
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import * as Fence from "@/server/shared/fence"
|
||||
|
||||
const ignoredMethods = new Set(["GET", "HEAD", "OPTIONS"])
|
||||
|
||||
export const fenceLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) =>
|
||||
export const fenceLayer = HttpRouter.middleware<{ requires: Database.Service; handles: unknown }>()(
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
if (!Flag.OPENCODE_WORKSPACE_ID || ignoredMethods.has(request.method)) return yield* effect
|
||||
const { db } = yield* Database.Service
|
||||
return (effect) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
if (!Flag.OPENCODE_WORKSPACE_ID || ignoredMethods.has(request.method)) return yield* effect
|
||||
|
||||
const previous = Fence.load()
|
||||
const response = yield* effect
|
||||
const current = Fence.diff(previous, Fence.load())
|
||||
if (Object.keys(current).length === 0) return response
|
||||
const previous = yield* Fence.load(db)
|
||||
const response = yield* effect
|
||||
const current = Fence.diff(previous, yield* Fence.load(db))
|
||||
if (Object.keys(current).length === 0) return response
|
||||
|
||||
return HttpServerResponse.setHeader(response, Fence.HEADER, JSON.stringify(current))
|
||||
return HttpServerResponse.setHeader(response, Fence.HEADER, JSON.stringify(current))
|
||||
})
|
||||
}),
|
||||
).layer
|
||||
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import type { Target } from "@/control-plane/types"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import { WorkspaceAdapterRuntime } from "@/control-plane/workspace-adapter-runtime"
|
||||
@@ -30,8 +30,8 @@ type RemoteTarget = Extract<Target, { type: "remote" }>
|
||||
|
||||
type RequestPlan = Data.TaggedEnum<{
|
||||
InvalidWorkspace: {}
|
||||
MissingWorkspace: { readonly workspaceID: WorkspaceID }
|
||||
Local: { readonly directory: string; readonly workspaceID?: WorkspaceID }
|
||||
MissingWorkspace: { readonly workspaceID: WorkspaceV2.ID }
|
||||
Local: { readonly directory: string; readonly workspaceID?: WorkspaceV2.ID }
|
||||
Remote: {
|
||||
readonly request: HttpServerRequest.HttpServerRequest
|
||||
readonly workspace: Workspace.Info
|
||||
@@ -46,7 +46,7 @@ export class WorkspaceRouteContext extends Context.Service<
|
||||
WorkspaceRouteContext,
|
||||
{
|
||||
readonly directory: string
|
||||
readonly workspaceID?: WorkspaceID
|
||||
readonly workspaceID?: WorkspaceV2.ID
|
||||
}
|
||||
>()("@opencode/ExperimentalHttpApiWorkspaceRouteContext") {}
|
||||
|
||||
@@ -62,23 +62,23 @@ function requestURL(request: HttpServerRequest.HttpServerRequest): URL {
|
||||
return new URL(request.url, "http://localhost")
|
||||
}
|
||||
|
||||
function configuredWorkspaceID(): WorkspaceID | undefined {
|
||||
return Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined
|
||||
function configuredWorkspaceID(): WorkspaceV2.ID | undefined {
|
||||
return Flag.OPENCODE_WORKSPACE_ID ? WorkspaceV2.ID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined
|
||||
}
|
||||
|
||||
function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceID): WorkspaceID | undefined {
|
||||
function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceV2.ID): WorkspaceV2.ID | undefined {
|
||||
const workspaceParam = url.searchParams.get("workspace")
|
||||
return sessionWorkspaceID ?? (workspaceParam ? WorkspaceID.make(workspaceParam) : undefined)
|
||||
return sessionWorkspaceID ?? (workspaceParam ? WorkspaceV2.ID.make(workspaceParam) : undefined)
|
||||
}
|
||||
|
||||
function selectedV2WorkspaceID(
|
||||
url: URL,
|
||||
sessionWorkspaceID?: WorkspaceID,
|
||||
): WorkspaceID | typeof InvalidWorkspaceID | undefined {
|
||||
sessionWorkspaceID?: WorkspaceV2.ID,
|
||||
): WorkspaceV2.ID | typeof InvalidWorkspaceID | undefined {
|
||||
if (sessionWorkspaceID) return sessionWorkspaceID
|
||||
const workspaceParam = url.searchParams.get("workspace")
|
||||
if (!workspaceParam) return undefined
|
||||
const workspaceID = Schema.decodeUnknownOption(WorkspaceID)(workspaceParam)
|
||||
const workspaceID = Schema.decodeUnknownOption(WorkspaceV2.ID)(workspaceParam)
|
||||
if (Option.isNone(workspaceID)) return InvalidWorkspaceID
|
||||
return workspaceID.value
|
||||
}
|
||||
@@ -92,14 +92,14 @@ function shouldStayOnControlPlane(request: HttpServerRequest.HttpServerRequest,
|
||||
}
|
||||
|
||||
function resolveWorkspace(
|
||||
id: WorkspaceID | undefined,
|
||||
envWorkspaceID: WorkspaceID | undefined,
|
||||
id: WorkspaceV2.ID | undefined,
|
||||
envWorkspaceID: WorkspaceV2.ID | undefined,
|
||||
): Effect.Effect<Workspace.Info | void, never, Workspace.Service> {
|
||||
if (!id || envWorkspaceID) return Effect.void
|
||||
return Workspace.Service.use((workspace) => workspace.get(id))
|
||||
}
|
||||
|
||||
function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServerResponse {
|
||||
function missingWorkspaceResponse(id: WorkspaceV2.ID): HttpServerResponse.HttpServerResponse {
|
||||
return HttpServerResponse.text(`Workspace not found: ${id}`, {
|
||||
status: 500,
|
||||
contentType: "text/plain; charset=utf-8",
|
||||
|
||||
@@ -13,7 +13,6 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Account } from "@/account/account"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { Auth } from "@/auth"
|
||||
import { Bus } from "@/bus"
|
||||
import { Config } from "@/config/config"
|
||||
import { Command } from "@/command"
|
||||
import * as Observability from "@opencode-ai/core/effect/observability"
|
||||
@@ -46,9 +45,9 @@ import { Todo } from "@/session/todo"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Skill } from "@/skill"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
@@ -191,8 +190,9 @@ export function createRoutes(
|
||||
errorLayer,
|
||||
compressionLayer,
|
||||
corsVaryFix,
|
||||
fenceLayer,
|
||||
fenceLayer.pipe(Layer.provide(Database.defaultLayer)),
|
||||
cors(corsOptions),
|
||||
Database.defaultLayer,
|
||||
Account.defaultLayer,
|
||||
Agent.defaultLayer,
|
||||
Auth.defaultLayer,
|
||||
@@ -225,7 +225,6 @@ export function createRoutes(
|
||||
SessionSummary.defaultLayer,
|
||||
ShareNext.defaultLayer,
|
||||
Snapshot.defaultLayer,
|
||||
SyncEvent.defaultLayer,
|
||||
EventV2Bridge.defaultLayer,
|
||||
Skill.defaultLayer,
|
||||
Todo.defaultLayer,
|
||||
@@ -233,7 +232,6 @@ export function createRoutes(
|
||||
Vcs.defaultLayer,
|
||||
Workspace.defaultLayer,
|
||||
Worktree.appLayer,
|
||||
Bus.layer,
|
||||
AppFileSystem.defaultLayer,
|
||||
FetchHttpClient.layer,
|
||||
HttpServer.layerServices,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Database } from "@/storage/db"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { inArray } from "drizzle-orm"
|
||||
import { EventSequenceTable } from "@/sync/event.sql"
|
||||
import { EventSequenceTable } from "@opencode-ai/core/event/sql"
|
||||
import { Workspace } from "@/control-plane/workspace"
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
|
||||
@@ -10,16 +10,16 @@ export const HEADER = "x-opencode-sync"
|
||||
export type State = Record<string, number>
|
||||
const log = Log.create({ service: "fence" })
|
||||
|
||||
export function load(ids?: string[]) {
|
||||
const rows = Database.use((db) => {
|
||||
if (!ids?.length) {
|
||||
return db.select().from(EventSequenceTable).all()
|
||||
}
|
||||
export function load(db: Database.Interface["db"], ids?: string[]) {
|
||||
return Effect.gen(function* () {
|
||||
const rows = yield* (
|
||||
ids?.length
|
||||
? db.select().from(EventSequenceTable).where(inArray(EventSequenceTable.aggregate_id, ids)).all()
|
||||
: db.select().from(EventSequenceTable).all()
|
||||
).pipe(Effect.orDie)
|
||||
|
||||
return db.select().from(EventSequenceTable).where(inArray(EventSequenceTable.aggregate_id, ids)).all()
|
||||
return Object.fromEntries(rows.map((row) => [row.aggregate_id, row.seq]))
|
||||
})
|
||||
|
||||
return Object.fromEntries(rows.map((row) => [row.aggregate_id, row.seq]))
|
||||
}
|
||||
|
||||
export function diff(prev: State, next: State) {
|
||||
@@ -53,7 +53,7 @@ export function parse(headers: Headers): State | undefined {
|
||||
)
|
||||
}
|
||||
|
||||
export function wait(workspaceID: WorkspaceID, state: State, signal?: AbortSignal) {
|
||||
export function wait(workspaceID: WorkspaceV2.ID, state: State, signal?: AbortSignal) {
|
||||
return Effect.gen(function* () {
|
||||
log.info("waiting for state", {
|
||||
workspaceID,
|
||||
|
||||
Reference in New Issue
Block a user