feat(workspace): support caller-supplied IDs (#44771)

This commit is contained in:
Kit Langton
2026-08-24 18:08:42 -04:00
committed by GitHub
parent 42d160f4a0
commit 22c63833d2
12 changed files with 1113 additions and 811 deletions
+7
View File
@@ -1691,6 +1691,12 @@ export interface WorktreeApi<E = never> {
readonly refresh: WorktreeRefreshOperation<E>
}
export type WorkspaceCreateInput = { readonly id?: Workspace.ID | undefined; readonly provider: string }
export type WorkspaceCreateOutput = Workspace.ID
export type WorkspaceCreateOperation<E = never> = (
input: WorkspaceCreateInput,
) => Effect.Effect<WorkspaceCreateOutput, E>
export type WorkspaceDestroyInput = { readonly workspaceID: Workspace.ID }
export type WorkspaceDestroyOutput = Workspace.DestroyResult
export type WorkspaceDestroyOperation<E = never> = (
@@ -1698,6 +1704,7 @@ export type WorkspaceDestroyOperation<E = never> = (
) => Effect.Effect<WorkspaceDestroyOutput, E>
export interface WorkspaceApi<E = never> {
readonly create: WorkspaceCreateOperation<E>
readonly destroy: WorkspaceDestroyOperation<E>
}
+14 -1
View File
@@ -214,6 +214,8 @@ import type {
WorktreeRemoveOutput,
WorktreeRefreshInput,
WorktreeRefreshOutput,
WorkspaceCreateInput,
WorkspaceCreateOutput,
WorkspaceDestroyInput,
WorkspaceDestroyOutput,
VcsGetInput,
@@ -1270,12 +1272,23 @@ const adaptGroupWorktree = (raw: RawClient["server.worktree"]) => ({
refresh: EndpointWorktreeRefresh(raw),
})
const EndpointWorkspaceCreate = (raw: RawClient["server.workspace"]) => (input: WorkspaceCreateInput) =>
preserveEffect<WorkspaceCreateOutput>()(
raw["workspace.create"]({ payload: { id: input["id"], provider: input["provider"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
)
const EndpointWorkspaceDestroy = (raw: RawClient["server.workspace"]) => (input: WorkspaceDestroyInput) =>
preserveEffect<WorkspaceDestroyOutput>()(
raw["workspace.destroy"]({ params: { workspaceID: input["workspaceID"] } }).pipe(Effect.mapError(mapClientError)),
)
const adaptGroupWorkspace = (raw: RawClient["server.workspace"]) => ({ destroy: EndpointWorkspaceDestroy(raw) })
const adaptGroupWorkspace = (raw: RawClient["server.workspace"]) => ({
create: EndpointWorkspaceCreate(raw),
destroy: EndpointWorkspaceDestroy(raw),
})
const EndpointVcsGet = (raw: RawClient["server.vcs"]) => (input?: VcsGetInput) =>
preserveEffect<VcsGetOutput>()(
@@ -210,6 +210,8 @@ import type {
WorktreeRemoveOutput,
WorktreeRefreshInput,
WorktreeRefreshOutput,
WorkspaceCreateInput,
WorkspaceCreateOutput,
WorkspaceDestroyInput,
WorkspaceDestroyOutput,
VcsGetInput,
@@ -1768,6 +1770,18 @@ export function make(options: ClientOptions) {
),
},
workspace: {
create: (input: WorkspaceCreateInput, requestOptions?: RequestOptions) =>
request<{ readonly data: WorkspaceCreateOutput }>(
{
method: "POST",
path: `/api/workspace`,
body: { id: input["id"], provider: input["provider"] },
successStatus: 200,
declaredStatuses: [409, 404, 401, 400],
empty: false,
},
requestOptions,
).then((value) => value.data),
destroy: (input: WorkspaceDestroyInput, requestOptions?: RequestOptions) =>
request<WorkspaceDestroyOutput>(
{
+755 -772
View File
@@ -20,6 +20,13 @@ export type SessionForkBoundary = { type: "before"; messageID: string } | { type
export type MoneyUSD = number
export type TokenUsageInfo = {
input: number
output: number
reasoning: number
cache: { read: number; write: number }
}
export type LocationRef = { directory: string; workspaceID?: string }
export type FileDiffInfo = {
@@ -43,22 +50,105 @@ export type SessionStatsToolUsage = {
export type SessionStatsActivity = { date: string; steps: number }
export type SessionMessageAgentSelected = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "agent-switched"
agent: string
previous?: string
}
export type PromptBase64 = string
export type PromptFileSource = { type: "inline" } | { type: "uri"; uri: string }
export type PromptMention = { start: number; end: number; text: string }
export type SessionMessageSynthetic = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
text: string
description?: string
type: "synthetic"
}
export type SessionMessageSystem = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "system"
text: string
description?: string
}
export type SessionMessageSkill = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "skill"
skill: string
name: string
text: string
}
export type SessionMessageShell = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number; completed?: number }
type: "shell"
shellID: string
command: string
status: "running" | "exited" | "timeout" | "killed"
exit?: number | "Infinity" | "-Infinity" | "NaN"
output?: { output: string; cursor: number; size: number; truncated: boolean }
}
export type SessionMessageProviderState = { [x: string]: JsonValue }
export type SessionMessageToolStateStreaming = { status: "streaming"; input: string }
export type SessionMessageToolStateRunning = {
status: "running"
input: { [x: string]: JsonValue }
metadata: { [x: string]: JsonValue }
}
export type ToolTextContent = { type: "text"; text: string }
export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }
export type SessionStructuredError = { type: string; message: string; status?: number }
export type SessionMessageCompactionRunning = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "running"
reason: "auto" | "manual"
summary: string
recent: string
}
export type SessionMessageCompactionCompleted = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "completed"
reason: "auto" | "manual"
summary: string
recent: string
}
export type SessionActive = { type: "running" }
export type SessionInboxDelivery = "steer" | "queue"
export type SessionInboxSyntheticPayload = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
export type SessionInboxCompactionPayload = {}
export type InstructionEntryKey = string
@@ -67,6 +157,19 @@ export type SessionGenerateResponse = { data: { text: string } }
export type SessionInboxSyntheticPayload1 = { text: string; description?: string; metadata?: { [x: string]: any } }
export type ShellInfo = {
id: string
status: "running" | "exited" | "timeout" | "killed"
command: string
cwd: string
shell: string
file: string
pid?: number
exit?: number
metadata: { [x: string]: any }
time: { started: number; completed?: number }
}
export type SessionMessageProviderState1 = { [x: string]: any }
export type ToolFileContent1 = { type: "file"; uri: string; mime: string; name?: string | undefined }
@@ -79,10 +182,41 @@ export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning
export type ModelMaxTokensField = "max_completion_tokens" | "max_tokens"
export type ModelCapabilities = {
tools: boolean
input: Array<string>
output: Array<string>
responsesWebsockets?: boolean
}
export type ModelVariant = {
id: string
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
}
export type MoneyUSDPerMillionTokens = number
export type GenerateTextResponse = { data: { text: string } }
export type ProviderInfo = {
id: string
integrationID?: string
name: string
activation: "auto" | "enabled" | "disabled"
package: string
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
}
export type FormWhen = {
key: string
op: "eq" | "neq"
value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}
export type FormOption = { value: string; label: string; description?: string }
export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string }
@@ -95,6 +229,50 @@ export type ConnectionCredentialInfo = { type: "credential"; id: string; label:
export type ConnectionEnvInfo = { type: "env"; name: string }
export type IntegrationAttemptStatus =
| {
status: "pending"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "complete"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "failed"
message: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "expired"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type IntegrationCommandAttempt = {
attemptID: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type IntegrationCommandAttemptStatus =
| {
status: "pending"
message?: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "complete"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "failed"
message: string
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
| {
status: "expired"
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
export type McpStatusConnected = { status: "connected" }
export type McpStatusPending = { status: "pending" }
@@ -125,6 +303,8 @@ export type ProjectTime = { created: number; updated: number; initialized?: numb
export type ProjectCurrent = { id: string; directory: string; canonical: string }
export type FormMetadata = { [x: string]: JsonValue }
export type FormValue = string | number | boolean | Array<string>
export type PermissionSource = { type: "tool"; messageID: string; id: string }
@@ -175,6 +355,19 @@ export type SessionStatus =
export type PtyTicketConnectToken = { ticket: string; expires_in: number }
export type ShellInfo1 = {
id: string
status: "running" | "exited" | "timeout" | "killed"
command: string
cwd: string
shell: string
file: string
pid?: number
exit?: number
metadata: { [x: string]: JsonValue }
time: { started: number; completed?: number }
}
export type ReferenceLocalSource = { type: "local"; path: string; description?: string; hidden?: boolean }
export type ReferenceGitSource = {
@@ -216,11 +409,15 @@ export type PluginInfo =
| { id: string; source: PluginSource; status: "active"; tui: boolean }
| { id?: string; source: PluginSource; status: "failed"; error: string; tui: boolean }
export type TokenUsageInfo = {
input: number
output: number
reasoning: number
cache: { read: number; write: number }
export type SessionMessageLocationSwitched = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "location-switched"
location: LocationRef
projectID?: string
subpath?: string
previous?: { location: LocationRef; projectID?: string; subpath?: string }
}
export type SessionInboxMovePayload = { location: LocationRef; projectID: string; subpath?: string }
@@ -233,23 +430,24 @@ export type V2EventServerConnected = {
data: {}
}
export type SessionRevert = { messageID: string; partID?: string; snapshot?: string; files?: Array<FileDiffInfo> }
export type SessionStatsTools =
| { mode: "none" }
| { mode: "summary"; totals: SessionStatsToolTotals }
| { mode: "detail"; totals: SessionStatsToolTotals; usage: Array<SessionStatsToolUsage> }
export type SessionMessageProviderState = { [x: string]: JsonValue }
export type SessionStatsModelUsage = { model: ModelRef; steps: number; tokens: TokenUsageInfo; cost: MoneyUSD }
export type SessionMessageToolStateRunning = {
status: "running"
input: { [x: string]: JsonValue }
metadata: { [x: string]: JsonValue }
export type SessionMessageModelSelected = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "model-switched"
model: ModelRef
previous?: ModelRef
}
export type SessionInboxSyntheticPayload = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
export type FormMetadata = { [x: string]: JsonValue }
export type PromptFileAttachment = {
data: PromptBase64
mime: string
@@ -263,10 +461,38 @@ export type PromptAgentAttachment = { name: string; mention?: PromptMention }
export type PromptSkillAttachment = { id: string; name: string; mention?: PromptMention }
export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }
export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState }
export type SessionMessageAssistantReasoning = {
type: "reasoning"
text: string
state?: SessionMessageProviderState
time?: { created: number; completed?: number }
}
export type ToolContent = ToolTextContent | ToolFileContent
export type SessionMessageAssistantRetry = { attempt: number; at: number; error: SessionStructuredError }
export type SessionMessageCompactionFailed = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "failed"
reason: "auto" | "manual"
error: SessionStructuredError
}
export type SessionInboxSynthetic = {
id: string
sessionID: string
timeCreated: number
type: "synthetic"
payload: SessionInboxSyntheticPayload
delivery: SessionInboxDelivery
}
export type SessionInboxCompaction = {
id: string
sessionID: string
@@ -511,6 +737,36 @@ export type SessionRetryScheduled = {
data: { sessionID: string; assistantMessageID: string; attempt: number; at: number; error: SessionStructuredError }
}
export type SessionCompactionStarted = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.started"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; reason: "auto" | "manual"; recent: string; inputID?: string }
}
export type SessionCompactionEnded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string }
}
export type SessionCompactionFailed = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.failed"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string }
}
export type SessionRevertCleared = {
id: string
created: number
@@ -531,6 +787,16 @@ export type SessionRevertCommitted = {
data: { sessionID: string; to: string }
}
export type SessionUsageRecorded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.usage.recorded"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; source: "title" | "compaction"; cost: MoneyUSD; tokens: TokenUsageInfo }
}
export type ModelsDevRefreshed = {
id: string
created: number
@@ -576,6 +842,15 @@ export type AgentUpdated = {
data: {}
}
export type SessionUsageUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.usage.updated"
location?: LocationRef
data: { sessionID: string; cost: MoneyUSD; tokens: TokenUsageInfo }
}
export type SessionTextDelta = {
id: string
created: number
@@ -872,30 +1147,78 @@ export type McpResourcesChanged = {
data: { server: string }
}
export type ShellInfo = {
export type SessionShellStarted = {
id: string
status: "running" | "exited" | "timeout" | "killed"
command: string
cwd: string
shell: string
file: string
pid?: number
exit?: number
metadata: { [x: string]: any }
time: { started: number; completed?: number }
created: number
metadata?: { [x: string]: any }
type: "session.shell.started"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; shell: ShellInfo }
}
export type ShellInfo1 = {
export type SessionShellEnded = {
id: string
status: "running" | "exited" | "timeout" | "killed"
command: string
cwd: string
shell: string
file: string
pid?: number
exit?: number
metadata: { [x: string]: JsonValue }
time: { started: number; completed?: number }
created: number
metadata?: { [x: string]: any }
type: "session.shell.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
shell: ShellInfo
output: { output: string; cursor: number; size: number; truncated: boolean }
}
}
export type ShellCreated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "shell.created"
location?: LocationRef
data: { info: ShellInfo }
}
export type SessionStepEnded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.step.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
rawFinish?: string
providerState?: SessionMessageProviderState1
cost: MoneyUSD
tokens: TokenUsageInfo
snapshot?: string
files?: Array<string>
}
}
export type SessionStepFailed = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.step.failed"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
error: SessionStructuredError
finish?: "content-filter"
rawFinish?: string
providerState?: SessionMessageProviderState1
cost?: MoneyUSD
tokens?: TokenUsageInfo
snapshot?: string
files?: Array<string>
}
}
export type SessionTextEnded = {
@@ -959,67 +1282,12 @@ export type SessionToolCalled = {
export type ToolContent1 = ToolTextContent | ToolFileContent1
export type SessionCompactionStarted = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.started"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; reason: "auto" | "manual"; recent: string; inputID?: string }
}
export type SessionCompactionEnded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string }
}
export type SessionCompactionFailed = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.failed"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string }
}
export type ModelCompatibility = {
reasoningField?: ModelReasoningField
maxTokensField?: ModelMaxTokensField
requireFinishReason?: boolean
}
export type ModelVariant = {
id: string
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
}
export type ProviderInfo = {
id: string
integrationID?: string
name: string
activation: "auto" | "enabled" | "disabled"
package: string
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
}
export type ModelCapabilities = {
tools: boolean
input: Array<string>
output: Array<string>
responsesWebsockets?: boolean
}
export type ModelCost = {
tier?: { type: "context"; size: number }
input: MoneyUSDPerMillionTokens
@@ -1027,6 +1295,71 @@ export type ModelCost = {
cache: { read: MoneyUSDPerMillionTokens; write: MoneyUSDPerMillionTokens }
}
export type FormNumberField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "number"
minimum?: number | "Infinity" | "-Infinity" | "NaN"
maximum?: number | "Infinity" | "-Infinity" | "NaN"
default?: number | "Infinity" | "-Infinity" | "NaN"
}
export type FormIntegerField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "integer"
minimum?: number | "Infinity" | "-Infinity" | "NaN"
maximum?: number | "Infinity" | "-Infinity" | "NaN"
default?: number | "Infinity" | "-Infinity" | "NaN"
}
export type FormBooleanField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "boolean"
default?: boolean
}
export type FormStringField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "string"
format?: "email" | "uri" | "date" | "date-time"
minLength?: number
maxLength?: number
pattern?: string
placeholder?: string
default?: string
options?: Array<FormOption>
custom?: boolean
}
export type FormMultiselectField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "multiselect"
options: Array<FormOption>
minItems?: number
maxItems?: number
custom?: boolean
default?: Array<string>
}
export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo
export type McpServer = {
@@ -1104,340 +1437,6 @@ export type PtyUpdated = {
data: { info: Pty }
}
export type SessionStatusUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.status"
location?: LocationRef
data: { sessionID: string; status: SessionStatus }
}
export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource
export type WorktreeList = Array<WorktreeDirectory>
export type VcsInfo = { branch: VcsBranch }
export type PermissionRuleset = Array<PermissionRule>
export type SessionStatsModelUsage = { model: ModelRef; steps: number; tokens: TokenUsageInfo; cost: MoneyUSD }
export type SessionStepEnded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.step.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
rawFinish?: string
providerState?: SessionMessageProviderState1
cost: MoneyUSD
tokens: TokenUsageInfo
snapshot?: string
files?: Array<string>
}
}
export type SessionUsageRecorded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.usage.recorded"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; source: "title" | "compaction"; cost: MoneyUSD; tokens: TokenUsageInfo }
}
export type SessionUsageUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.usage.updated"
location?: LocationRef
data: { sessionID: string; cost: MoneyUSD; tokens: TokenUsageInfo }
}
export type SessionStepFailed = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.step.failed"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
error: SessionStructuredError
finish?: "content-filter"
rawFinish?: string
providerState?: SessionMessageProviderState1
cost?: MoneyUSD
tokens?: TokenUsageInfo
snapshot?: string
files?: Array<string>
}
}
export type SessionInboxMove = {
id: string
sessionID: string
timeCreated: number
type: "move"
payload: SessionInboxMovePayload
delivery: SessionInboxDelivery
}
export type SessionRevert = { messageID: string; partID?: string; snapshot?: string; files?: Array<FileDiffInfo> }
export type SessionMessageAgentSelected = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "agent-switched"
agent: string
previous?: string
}
export type SessionMessageModelSelected = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "model-switched"
model: ModelRef
previous?: ModelRef
}
export type SessionMessageLocationSwitched = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "location-switched"
location: LocationRef
projectID?: string
subpath?: string
previous?: { location: LocationRef; projectID?: string; subpath?: string }
}
export type SessionMessageSynthetic = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
text: string
description?: string
type: "synthetic"
}
export type SessionMessageSystem = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "system"
text: string
description?: string
}
export type SessionMessageSkill = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
type: "skill"
skill: string
name: string
text: string
}
export type SessionMessageShell = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number; completed?: number }
type: "shell"
shellID: string
command: string
status: "running" | "exited" | "timeout" | "killed"
exit?: number | ("Infinity" | "-Infinity" | "NaN")
output?: { output: string; cursor: number; size: number; truncated: boolean }
}
export type SessionMessageCompactionRunning = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "running"
reason: "auto" | "manual"
summary: string
recent: string
}
export type SessionMessageCompactionCompleted = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "completed"
reason: "auto" | "manual"
summary: string
recent: string
}
export type SessionMessageCompactionFailed = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "failed"
reason: "auto" | "manual"
error: SessionStructuredError
}
export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState }
export type SessionMessageAssistantReasoning = {
type: "reasoning"
text: string
state?: SessionMessageProviderState
time?: { created: number; completed?: number }
}
export type SessionInboxSynthetic = {
id: string
sessionID: string
timeCreated: number
type: "synthetic"
payload: SessionInboxSyntheticPayload
delivery: SessionInboxDelivery
}
export type FormWhen = {
key: string
op: "eq" | "neq"
value: string | (number | ("Infinity" | "-Infinity" | "NaN")) | boolean
}
export type ToolContent = ToolTextContent | ToolFileContent
export type SessionForked = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.forked"
durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef
data: {
sessionID: string
parentID: string
boundary: SessionForkBoundary
instructions?: { [x: string]: string }
instructionEntries?: InstructionEntrySnapshot
}
}
export type SessionShellStarted = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.shell.started"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; shell: ShellInfo }
}
export type SessionShellEnded = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.shell.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: {
sessionID: string
shell: ShellInfo
output: { output: string; cursor: number; size: number; truncated: boolean }
}
}
export type ShellCreated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "shell.created"
location?: LocationRef
data: { info: ShellInfo }
}
export type SessionToolSuccess = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.tool.success"
durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
id: string
content: [ToolContent1, ...Array<ToolContent1>]
metadata?: { [x: string]: JsonValue }
executed: boolean
resultState?: SessionMessageProviderState1
}
}
export type SessionToolFailed = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.tool.failed"
durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
id: string
error: SessionStructuredError
content?: [ToolContent1, ...Array<ToolContent1>]
metadata?: { [x: string]: JsonValue }
executed: boolean
resultState?: SessionMessageProviderState1
}
}
export type ModelInfo = {
id: string
modelID: string
providerID: string
family?: string
name: string
compatibility?: ModelCompatibility
package?: string
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
capabilities: ModelCapabilities
variants: Array<ModelVariant>
time: { released: number }
cost: Array<ModelCost>
status: "alpha" | "beta" | "deprecated" | "active"
enabled: boolean
limit: { context: number; input?: number; output: number }
}
export type FormState = { status: "pending" } | { status: "answered"; answer: FormAnswer } | { status: "cancelled" }
export type FormReplied = {
id: string
created: number
metadata?: { [x: string]: any }
type: "form.replied"
location?: LocationRef
data: { id: string; sessionID: string; answer: FormAnswer }
}
export type FormStringField1 = {
key: string
title?: string
@@ -1503,6 +1502,221 @@ export type FormMultiselectField1 = {
default?: Array<string>
}
export type SessionStatusUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.status"
location?: LocationRef
data: { sessionID: string; status: SessionStatus }
}
export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource
export type WorktreeList = Array<WorktreeDirectory>
export type VcsInfo = { branch: VcsBranch }
export type PermissionRuleset = Array<PermissionRule>
export type SessionInboxMove = {
id: string
sessionID: string
timeCreated: number
type: "move"
payload: SessionInboxMovePayload
delivery: SessionInboxDelivery
}
export type SessionInfo = {
id: string
parentID?: string
fork?: { sessionID: string; boundary: SessionForkBoundary }
projectID: string
agent?: string
model?: ModelRef
cost: MoneyUSD
tokens: TokenUsageInfo
outcome?: "succeeded" | "failed" | "interrupted"
time: { created: number; updated: number; idle?: number; viewed?: number; archived?: number }
title?: string
location: LocationRef
subpath?: string
revert?: SessionRevert
}
export type SessionRevertStaged = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.revert.staged"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; revert: SessionRevert }
}
export type SessionStatsInfo = {
range: { from: number; to: number }
sessions: number
subagents: number
prompts: number
steps: number
tokens: TokenUsageInfo
cost: MoneyUSD
tools: SessionStatsTools
activeDays: number
streak: number
activity: Array<SessionStatsActivity>
models: Array<SessionStatsModelUsage>
}
export type SessionMessageUser = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
type: "user"
}
export type SessionInboxUserPayload = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
metadata?: { [x: string]: JsonValue }
}
export type SessionInboxUserPayload1 = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
metadata?: { [x: string]: any }
}
export type SessionMessageToolStateCompleted = {
status: "completed"
input: { [x: string]: JsonValue }
content: [ToolContent, ...Array<ToolContent>]
metadata?: { [x: string]: JsonValue }
}
export type SessionMessageToolStateError = {
status: "error"
input: { [x: string]: JsonValue }
error: SessionStructuredError
content?: [ToolContent, ...Array<ToolContent>]
metadata?: { [x: string]: JsonValue }
}
export type SessionMessageCompaction =
| SessionMessageCompactionRunning
| SessionMessageCompactionCompleted
| SessionMessageCompactionFailed
export type SessionForked = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.forked"
durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef
data: {
sessionID: string
parentID: string
boundary: SessionForkBoundary
instructions?: { [x: string]: string }
instructionEntries?: InstructionEntrySnapshot
}
}
export type SessionToolSuccess = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.tool.success"
durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
id: string
content: [ToolContent1, ...Array<ToolContent1>]
metadata?: { [x: string]: JsonValue }
executed: boolean
resultState?: SessionMessageProviderState1
}
}
export type SessionToolFailed = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.tool.failed"
durable: { aggregateID: string; seq: number; version: 2 }
location?: LocationRef
data: {
sessionID: string
assistantMessageID: string
id: string
error: SessionStructuredError
content?: [ToolContent1, ...Array<ToolContent1>]
metadata?: { [x: string]: JsonValue }
executed: boolean
resultState?: SessionMessageProviderState1
}
}
export type ModelInfo = {
id: string
modelID: string
providerID: string
family?: string
name: string
compatibility?: ModelCompatibility
package?: string
settings?: { [x: string]: any }
headers?: { [x: string]: string }
body?: { [x: string]: any }
capabilities: ModelCapabilities
variants: Array<ModelVariant>
time: { released: number }
cost: Array<ModelCost>
status: "alpha" | "beta" | "deprecated" | "active"
enabled: boolean
limit: { context: number; input?: number; output: number }
}
export type FormField =
| FormStringField
| FormNumberField
| FormIntegerField
| FormBooleanField
| FormMultiselectField
| FormExternalField
export type FormState = { status: "pending" } | { status: "answered"; answer: FormAnswer } | { status: "cancelled" }
export type FormReplied = {
id: string
created: number
metadata?: { [x: string]: any }
type: "form.replied"
location?: LocationRef
data: { id: string; sessionID: string; answer: FormAnswer }
}
export type FormField1 =
| FormStringField1
| FormNumberField1
| FormIntegerField1
| FormBooleanField1
| FormMultiselectField1
| FormExternalField
export type ReferenceInfo = {
name: string
path: string
@@ -1690,171 +1904,6 @@ export type ConfigEntry =
| { type: "agents"; path: string }
| { type: "claude"; path: string }
export type SessionStatsInfo = {
range: { from: number; to: number }
sessions: number
subagents: number
prompts: number
steps: number
tokens: TokenUsageInfo
cost: MoneyUSD
tools: SessionStatsTools
activeDays: number
streak: number
activity: Array<SessionStatsActivity>
models: Array<SessionStatsModelUsage>
}
export type SessionInfo = {
id: string
parentID?: string
fork?: { sessionID: string; boundary: SessionForkBoundary }
projectID: string
agent?: string
model?: ModelRef
cost: MoneyUSD
tokens: TokenUsageInfo
outcome?: "succeeded" | "failed" | "interrupted"
time: { created: number; updated: number; idle?: number; viewed?: number; archived?: number }
title?: string
location: LocationRef
subpath?: string
revert?: SessionRevert
}
export type SessionRevertStaged = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.revert.staged"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; revert: SessionRevert }
}
export type SessionMessageCompaction =
| SessionMessageCompactionRunning
| SessionMessageCompactionCompleted
| SessionMessageCompactionFailed
export type SessionMessageUser = {
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
type: "user"
}
export type SessionInboxUserPayload = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
metadata?: { [x: string]: JsonValue }
}
export type SessionInboxUserPayload1 = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
metadata?: { [x: string]: any }
}
export type IntegrationAttemptStatus =
| {
status: "pending"
time: {
created: number | ("Infinity" | "-Infinity" | "NaN")
expires: number | ("Infinity" | "-Infinity" | "NaN")
}
}
| {
status: "complete"
time: {
created: number | ("Infinity" | "-Infinity" | "NaN")
expires: number | ("Infinity" | "-Infinity" | "NaN")
}
}
| {
status: "failed"
message: string
time: {
created: number | ("Infinity" | "-Infinity" | "NaN")
expires: number | ("Infinity" | "-Infinity" | "NaN")
}
}
| {
status: "expired"
time: {
created: number | ("Infinity" | "-Infinity" | "NaN")
expires: number | ("Infinity" | "-Infinity" | "NaN")
}
}
export type IntegrationCommandAttempt = {
attemptID: string
time: { created: number | ("Infinity" | "-Infinity" | "NaN"); expires: number | ("Infinity" | "-Infinity" | "NaN") }
}
export type IntegrationCommandAttemptStatus =
| {
status: "pending"
message?: string
time: {
created: number | ("Infinity" | "-Infinity" | "NaN")
expires: number | ("Infinity" | "-Infinity" | "NaN")
}
}
| {
status: "complete"
time: {
created: number | ("Infinity" | "-Infinity" | "NaN")
expires: number | ("Infinity" | "-Infinity" | "NaN")
}
}
| {
status: "failed"
message: string
time: {
created: number | ("Infinity" | "-Infinity" | "NaN")
expires: number | ("Infinity" | "-Infinity" | "NaN")
}
}
| {
status: "expired"
time: {
created: number | ("Infinity" | "-Infinity" | "NaN")
expires: number | ("Infinity" | "-Infinity" | "NaN")
}
}
export type SessionMessageToolStateCompleted = {
status: "completed"
input: { [x: string]: JsonValue }
content: [ToolContent, ...Array<ToolContent>]
metadata?: { [x: string]: JsonValue }
}
export type SessionMessageToolStateError = {
status: "error"
input: { [x: string]: JsonValue }
error: SessionStructuredError
content?: [ToolContent, ...Array<ToolContent>]
metadata?: { [x: string]: JsonValue }
}
export type FormField1 =
| FormStringField1
| FormNumberField1
| FormIntegerField1
| FormBooleanField1
| FormMultiselectField1
| FormExternalField
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
export type SessionInboxUser = {
@@ -1872,71 +1921,6 @@ export type SessionInboxItem =
| { type: "compaction"; payload: SessionInboxCompactionPayload; delivery: SessionInboxDelivery }
| { type: "move"; payload: SessionInboxMovePayload; delivery: SessionInboxDelivery }
export type FormStringField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "string"
format?: "email" | "uri" | "date" | "date-time"
minLength?: number
maxLength?: number
pattern?: string
placeholder?: string
default?: string
options?: Array<FormOption>
custom?: boolean
}
export type FormNumberField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "number"
minimum?: number | ("Infinity" | "-Infinity" | "NaN")
maximum?: number | ("Infinity" | "-Infinity" | "NaN")
default?: number | ("Infinity" | "-Infinity" | "NaN")
}
export type FormIntegerField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "integer"
minimum?: number | ("Infinity" | "-Infinity" | "NaN")
maximum?: number | ("Infinity" | "-Infinity" | "NaN")
default?: number | ("Infinity" | "-Infinity" | "NaN")
}
export type FormBooleanField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "boolean"
default?: boolean
}
export type FormMultiselectField = {
key: string
title?: string
description?: string
required?: boolean
when?: Array<FormWhen>
type: "multiselect"
options: Array<FormOption>
minItems?: number
maxItems?: number
custom?: boolean
default?: Array<string>
}
export type SessionMessageAssistantTool = {
type: "tool"
id: string
@@ -1952,6 +1936,8 @@ export type SessionMessageAssistantTool = {
time: { created: number; ran?: number; completed?: number }
}
export type FormFields = [FormField, ...Array<FormField>]
export type FormFields2 = [FormField1, ...Array<FormField1>]
export type SessionInboxInfo = SessionInboxUser | SessionInboxSynthetic | SessionInboxCompaction | SessionInboxMove
@@ -1966,14 +1952,6 @@ export type SessionInboxEnqueued = {
data: { sessionID: string; inboxID: string; item: SessionInboxItem }
}
export type FormField =
| FormStringField
| FormNumberField
| FormIntegerField
| FormBooleanField
| FormMultiselectField
| FormExternalField
export type SessionMessageAssistant = {
id: string
metadata?: { [x: string]: JsonValue }
@@ -1992,6 +1970,12 @@ export type SessionMessageAssistant = {
retry?: SessionMessageAssistantRetry
}
export type IntegrationOAuthMethod = { id: string; type: "oauth"; label: string; form?: FormFields }
export type IntegrationKeyMethod = { type: "key"; label?: string; form?: FormFields }
export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields }
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields2 }
export type SessionEventDurable =
@@ -2037,8 +2021,6 @@ export type SessionEventDurable =
| SessionRevertCommitted
| SessionUsageRecorded
export type FormFields = [FormField, ...Array<FormField>]
export type SessionMessageInfo =
| SessionMessageAgentSelected
| SessionMessageModelSelected
@@ -2051,6 +2033,12 @@ export type SessionMessageInfo =
| SessionMessageAssistant
| SessionMessageCompaction
export type IntegrationMethod =
| IntegrationOAuthMethod
| IntegrationCommandMethod
| IntegrationKeyMethod
| IntegrationEnvMethod
export type FormCreated = {
id: string
created: number
@@ -2062,12 +2050,6 @@ export type FormCreated = {
export type SessionLogItem = SessionEventDurable | EventLogSynced
export type IntegrationOAuthMethod = { id: string; type: "oauth"; label: string; form?: FormFields }
export type IntegrationKeyMethod = { type: "key"; label?: string; form?: FormFields }
export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields }
export type SessionTransferData = { info: SessionInfo; messages: Array<SessionMessageInfo> }
export type SessionMessagesResponse = {
@@ -2075,6 +2057,13 @@ export type SessionMessagesResponse = {
cursor: { previous?: string | null; next?: string | null }
}
export type IntegrationInfo = {
id: string
name: string
methods: Array<IntegrationMethod>
connections: Array<ConnectionInfo>
}
export type V2Event =
| ModelsDevRefreshed
| IntegrationUpdated
@@ -2162,19 +2151,6 @@ export type V2Event =
| McpResourcesChanged
| V2EventServerConnected
export type IntegrationMethod =
| IntegrationOAuthMethod
| IntegrationCommandMethod
| IntegrationKeyMethod
| IntegrationEnvMethod
export type IntegrationInfo = {
id: string
name: string
methods: Array<IntegrationMethod>
connections: Array<ConnectionInfo>
}
export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string }
export const isUnauthorizedError = (value: unknown): value is UnauthorizedError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError"
@@ -4149,7 +4125,7 @@ export type IntegrationOauthConnectOutput = {
url: string
instructions: string
mode: "auto" | "code"
time: { created: number | ("Infinity" | "-Infinity" | "NaN"); expires: number | ("Infinity" | "-Infinity" | "NaN") }
time: { created: number | "Infinity" | "-Infinity" | "NaN"; expires: number | "Infinity" | "-Infinity" | "NaN" }
}
}
@@ -4371,7 +4347,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
@@ -4395,12 +4371,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "number"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4410,12 +4386,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "integer"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4425,7 +4401,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "boolean"
readonly default?: boolean
@@ -4438,7 +4414,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "multiselect"
readonly options: ReadonlyArray<{
@@ -4468,7 +4444,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
@@ -4492,12 +4468,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "number"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4507,12 +4483,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "integer"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4522,7 +4498,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "boolean"
readonly default?: boolean
@@ -4535,7 +4511,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "multiselect"
readonly options: ReadonlyArray<{
@@ -4572,7 +4548,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
@@ -4596,12 +4572,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "number"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4611,12 +4587,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "integer"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4626,7 +4602,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "boolean"
readonly default?: boolean
@@ -4639,7 +4615,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "multiselect"
readonly options: ReadonlyArray<{
@@ -4669,7 +4645,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
@@ -4693,12 +4669,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "number"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4708,12 +4684,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "integer"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4723,7 +4699,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "boolean"
readonly default?: boolean
@@ -4736,7 +4712,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "multiselect"
readonly options: ReadonlyArray<{
@@ -4773,7 +4749,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
@@ -4797,12 +4773,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "number"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4812,12 +4788,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "integer"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4827,7 +4803,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "boolean"
readonly default?: boolean
@@ -4840,7 +4816,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "multiselect"
readonly options: ReadonlyArray<{
@@ -4870,7 +4846,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
@@ -4894,12 +4870,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "number"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4909,12 +4885,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "integer"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -4924,7 +4900,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "boolean"
readonly default?: boolean
@@ -4937,7 +4913,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "multiselect"
readonly options: ReadonlyArray<{
@@ -4974,7 +4950,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
@@ -4998,12 +4974,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "number"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -5013,12 +4989,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "integer"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -5028,7 +5004,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "boolean"
readonly default?: boolean
@@ -5041,7 +5017,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "multiselect"
readonly options: ReadonlyArray<{
@@ -5071,7 +5047,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
@@ -5095,12 +5071,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "number"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -5110,12 +5086,12 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "integer"
readonly minimum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly maximum?: number | ("Infinity" | "-Infinity" | "NaN")
readonly default?: number | ("Infinity" | "-Infinity" | "NaN")
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
@@ -5125,7 +5101,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "boolean"
readonly default?: boolean
@@ -5138,7 +5114,7 @@ export type FormCreateInput = {
readonly when?: ReadonlyArray<{
readonly key: string
readonly op: "eq" | "neq"
readonly value: string | number | ("Infinity" | "-Infinity" | "NaN") | boolean
readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}>
readonly type: "multiselect"
readonly options: ReadonlyArray<{
@@ -5654,6 +5630,13 @@ export type WorktreeRefreshInput = { readonly projectID: { readonly projectID: s
export type WorktreeRefreshOutput = void
export type WorkspaceCreateInput = {
readonly id?: { readonly id?: string | undefined; readonly provider: string }["id"]
readonly provider: { readonly id?: string | undefined; readonly provider: string }["provider"]
}
export type WorkspaceCreateOutput = { data: string }["data"]
export type WorkspaceDestroyInput = { readonly workspaceID: { readonly workspaceID: string }["workspaceID"] }
export type WorkspaceDestroyOutput = WorkspaceDestroyResult
+42 -9
View File
@@ -25,9 +25,18 @@ export class Info extends Schema.Class<Info>("Workspace.Info")({
export class NotFound extends Schema.TaggedError<NotFound>()("Workspace.NotFound", { workspaceID: ID }) {}
export class CreateConflict extends Schema.TaggedError<CreateConflict>()("Workspace.CreateConflict", {
workspaceID: ID,
provider: Schema.String,
existingProvider: Schema.String,
}) {}
export interface Interface {
/** Instantly commits a logical workspace ID. No provider work happens here. */
readonly create: (provider: string) => Effect.Effect<ID, WorkspaceDriver.ProviderNotFound>
readonly create: (input: {
readonly id?: ID
readonly provider: string
}) => Effect.Effect<ID, CreateConflict | WorkspaceDriver.ProviderNotFound>
/** Starts or joins the shared attempt that makes the backing resource real, then returns it. */
readonly provision: (
workspaceID: ID,
@@ -212,15 +221,39 @@ const layer = (options: Options) =>
}).pipe(Effect.repeat(Schedule.spaced(options.pollInterval ?? Duration.minutes(1))), Effect.forkScoped)
return Service.of({
create: Effect.fn("Workspace.create")(function* (provider) {
yield* registry.get(provider)
const workspaceID = ID.create()
const now = yield* Clock.currentTimeMillis
yield* db
.insert(WorkspaceTable)
.values({ id: workspaceID, provider, binding: null, created_at: now, last_used_at: now })
.run()
create: Effect.fn("Workspace.create")(function* (input) {
const workspaceID = input.id ?? ID.create()
const existing = yield* db
.select({ provider: WorkspaceTable.provider })
.from(WorkspaceTable)
.where(eq(WorkspaceTable.id, workspaceID))
.get()
.pipe(Effect.orDie)
if (existing) {
if (existing.provider === input.provider) return workspaceID
return yield* new CreateConflict({
workspaceID,
provider: input.provider,
existingProvider: existing.provider,
})
}
yield* registry.get(input.provider)
const now = yield* Clock.currentTimeMillis
const inserted = yield* db
.insert(WorkspaceTable)
.values({ id: workspaceID, provider: input.provider, binding: null, created_at: now, last_used_at: now })
.onConflictDoNothing()
.returning({ id: WorkspaceTable.id })
.get()
.pipe(Effect.orDie)
if (inserted) return workspaceID
const row = yield* load(workspaceID).pipe(Effect.orDie)
if (row.provider !== input.provider)
return yield* new CreateConflict({
workspaceID,
provider: input.provider,
existingProvider: row.provider,
})
return workspaceID
}),
provision,
+76 -12
View File
@@ -41,7 +41,7 @@ const driver = WorkspaceDriver.make({
const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([Database.node, Workspace.configured({ idleThreshold: "5 minutes", pollInterval: "1 minute" })]),
[[WorkspaceDriver.node, WorkspaceDriver.registryNode({ fake: driver })]],
[[WorkspaceDriver.node, WorkspaceDriver.registryNode({ fake: driver, other: driver })]],
),
)
@@ -77,7 +77,7 @@ it.effect("rejects unregistered workspace providers", () =>
it.effect("creates and persists an ID without provisioning", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
expect(workspaceID.startsWith("wrk_")).toBe(true)
expect(calls).toEqual([])
@@ -89,10 +89,74 @@ it.effect("creates and persists an ID without provisioning", () =>
}),
)
it.effect("creates a workspace with a caller-supplied ID", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const id = Workspace.ID.create()
expect(yield* workspace.create({ id, provider: "fake" })).toBe(id)
expect(
yield* Database.Service.use(({ db }) =>
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get(),
).pipe(Effect.orDie),
).toMatchObject({ id, provider: "fake", binding: null })
}),
)
it.effect("reuses a caller-supplied ID with the same provider", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const id = Workspace.ID.create()
expect(yield* workspace.create({ id, provider: "fake" })).toBe(id)
expect(yield* workspace.create({ id, provider: "fake" })).toBe(id)
expect(
yield* Database.Service.use(({ db }) =>
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).all(),
).pipe(Effect.orDie),
).toHaveLength(1)
expect(calls).toEqual([])
}),
)
it.effect("rejects a caller-supplied ID already assigned to another provider", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const id = Workspace.ID.create()
yield* workspace.create({ id, provider: "fake" })
expect(yield* workspace.create({ id, provider: "other" }).pipe(Effect.flip)).toEqual(
new Workspace.CreateConflict({ workspaceID: id, provider: "other", existingProvider: "fake" }),
)
}),
)
it.effect("resolves an existing caller-supplied ID before provider lookup", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const id = Workspace.ID.create()
yield* Database.Service.use(({ db }) =>
db
.insert(WorkspaceTable)
.values({ id, provider: "missing", binding: null, created_at: 0, last_used_at: 0 })
.run(),
).pipe(Effect.orDie)
expect(yield* workspace.create({ id, provider: "missing" })).toBe(id)
expect(yield* workspace.create({ id, provider: "another-missing" }).pipe(Effect.flip)).toEqual(
new Workspace.CreateConflict({
workspaceID: id,
provider: "another-missing",
existingProvider: "missing",
}),
)
}),
)
it.effect("destroys an unprovisioned workspace through the driver with a null binding", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
expect(yield* workspace.destroy(workspaceID)).toEqual({ destroyed: true })
expect(calls).toEqual([{ operation: "destroy", binding: null }])
@@ -117,7 +181,7 @@ it.effect("succeeds without calling the driver when the workspace does not exist
it.effect("reports whether destroy removed an existing workspace", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
expect(yield* workspace.destroy(workspaceID)).toEqual({ destroyed: true })
expect(yield* workspace.destroy(workspaceID)).toEqual({ destroyed: false })
@@ -128,7 +192,7 @@ it.effect("reports whether destroy removed an existing workspace", () =>
it.effect("starts eager provisioning in the background and lets callers join it", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
const gate = yield* gateCreate()
const eager = yield* workspace.provision(workspaceID).pipe(Effect.forkScoped({ startImmediately: true }))
@@ -147,7 +211,7 @@ it.effect("starts eager provisioning in the background and lets callers join it"
it.effect("starts lazy provisioning on the first spawn", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
const environment = yield* workspace.connect(workspaceID)
const gate = yield* gateCreate()
@@ -166,7 +230,7 @@ it.effect("starts lazy provisioning on the first spawn", () =>
it.effect("shares provisioning between concurrent first spawns", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
const environment = yield* workspace.connect(workspaceID)
const gate = yield* gateCreate()
@@ -190,7 +254,7 @@ it.effect("shares provisioning between concurrent first spawns", () =>
it.effect("keeps shared provisioning alive when a waiter is interrupted", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
const gate = yield* gateCreate()
const owner = yield* workspace.provision(workspaceID).pipe(Effect.forkScoped({ startImmediately: true }))
@@ -208,7 +272,7 @@ it.effect("keeps shared provisioning alive when a waiter is interrupted", () =>
it.effect("interrupts in-flight provisioning on destroy and fails waiters with NotFound", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
const gate = yield* gateCreate()
const waiter = yield* workspace.provision(workspaceID).pipe(Effect.forkScoped({ startImmediately: true }))
@@ -229,7 +293,7 @@ it.effect("interrupts in-flight provisioning on destroy and fails waiters with N
it.effect("shares a failed attempt and retries the same workspace ID", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
const started = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
let fail = true
@@ -263,7 +327,7 @@ it.effect("shares a failed attempt and retries the same workspace ID", () =>
it.effect("persists the workspace lifecycle and reconnects after idle suspension", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const workspaceID = yield* workspace.create("fake")
const workspaceID = yield* workspace.create({ provider: "fake" })
const created = yield* workspace.provision(workspaceID)
expect(created.id).toBe(workspaceID)
@@ -298,7 +362,7 @@ it.effect("persists the workspace lifecycle and reconnects after idle suspension
it.effect("surfaces wake failures through the spawn error channel", () =>
Effect.gen(function* () {
const workspace = yield* Workspace.Service
const created = yield* workspace.provision(yield* workspace.create("fake"))
const created = yield* workspace.provision(yield* workspace.create({ provider: "fake" }))
const environment = yield* workspace.connect(created.id)
yield* Effect.scoped(environment.spawner.spawn(ChildProcess.make("connect"))).pipe(Effect.exit)
+98
View File
@@ -10614,6 +10614,104 @@
"summary": "Refresh worktrees"
}
},
"/api/workspace": {
"post": {
"tags": ["workspace"],
"operationId": "v2.workspace.create",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"pattern": "^wrk"
}
},
"required": ["data"],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
},
"404": {
"description": "ProviderNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderNotFoundErrorEncoded"
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictErrorEncoded"
}
}
}
}
},
"description": "Create a logical workspace. A caller-supplied ID is idempotent when retried with the same provider; reusing it with another provider returns a conflict.",
"summary": "Create workspace",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string",
"pattern": "^wrk"
},
{
"type": "null"
}
]
},
"provider": {
"type": "string"
}
},
"required": ["provider"],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/workspace/{workspaceID}": {
"delete": {
"tags": ["workspace"],
+19 -1
View File
@@ -1,8 +1,26 @@
import { Workspace } from "@opencode-ai/schema/workspace"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { UnknownError } from "../errors.js"
import { ConflictError, ProviderNotFoundError, UnknownError } from "../errors.js"
export const WorkspaceGroup = HttpApiGroup.make("server.workspace")
.add(
HttpApiEndpoint.post("workspace.create", "/api/workspace", {
payload: Schema.Struct({
id: Workspace.ID.pipe(Schema.optional),
provider: Schema.String,
}),
success: Schema.Struct({ data: Workspace.ID }),
error: [ConflictError, ProviderNotFoundError],
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.workspace.create",
summary: "Create workspace",
description:
"Create a logical workspace. A caller-supplied ID is idempotent when retried with the same provider; reusing it with another provider returns a conflict.",
}),
),
)
.add(
HttpApiEndpoint.delete("workspace.destroy", "/api/workspace/:workspaceID", {
params: { workspaceID: Workspace.ID },
+2 -2
View File
@@ -16,7 +16,7 @@ export type Interface = Omit<OpenCodeClient, "plugin" | "workspace"> & {
readonly sessions: OpenCodeClient["session"]
readonly events: OpenCodeClient["event"]
readonly workspace: {
readonly create: (options: { readonly provider: string }) => ReturnType<Workspace.Interface["create"]>
readonly create: Workspace.Interface["create"]
readonly provision: (options: {
readonly workspaceID: Workspace.ID
}) => ReturnType<Workspace.Interface["provision"]>
@@ -44,7 +44,7 @@ export const create: (
sessions: client.session,
events: client.event,
workspace: {
create: ({ provider }: { readonly provider: string }) => host.workspace.create(provider),
create: host.workspace.create,
provision: ({ workspaceID }: { readonly workspaceID: Workspace.ID }) => host.workspace.provision(workspaceID),
destroy: ({ workspaceID }: { readonly workspaceID: Workspace.ID }) => host.workspace.destroy(workspaceID),
},
+4 -1
View File
@@ -462,8 +462,11 @@ it.live("configures workspace providers through the SDK facade", () =>
},
})
const opencode = yield* fixture.sdk.OpenCode.create({ workspaceProviders: { fake: driver } })
const workspaceID = yield* opencode.workspace.create({ provider: "fake" })
const requestedID = fixture.sdk.Workspace.ID.create()
const workspaceID = yield* opencode.workspace.create({ id: requestedID, provider: "fake" })
expect(workspaceID).toBe(requestedID)
expect(yield* opencode.workspace.create({ id: requestedID, provider: "fake" })).toBe(requestedID)
expect(calls).toEqual([])
const workspace = yield* opencode.workspace.provision({ workspaceID })
+31 -13
View File
@@ -1,5 +1,5 @@
import { Workspace } from "@opencode-ai/core/workspace"
import { UnknownError } from "@opencode-ai/protocol/errors"
import { ConflictError, ProviderNotFoundError, UnknownError } from "@opencode-ai/protocol/errors"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
@@ -8,18 +8,36 @@ export const WorkspaceHandler = HttpApiBuilder.group(Api, "server.workspace", (h
Effect.gen(function* () {
const workspace = yield* Workspace.Service
return handlers.handle("workspace.destroy", (ctx) =>
workspace.destroy(ctx.params.workspaceID).pipe(
Effect.mapError(
(error) =>
new UnknownError({
message:
error._tag === "WorkspaceDriver.ProviderNotFound"
? `Workspace provider not found: ${error.provider}`
: (error.message ?? "Workspace provider failed to destroy the workspace"),
}),
return handlers
.handle("workspace.create", (ctx) =>
workspace.create(ctx.payload).pipe(
Effect.map((workspaceID) => ({ data: workspaceID })),
Effect.catchTags({
"Workspace.CreateConflict": (error) =>
new ConflictError({
resource: error.workspaceID,
message: `Workspace ${error.workspaceID} already uses provider ${error.existingProvider}, not ${error.provider}`,
}),
"WorkspaceDriver.ProviderNotFound": (error) =>
new ProviderNotFoundError({
providerID: error.provider,
message: `Workspace provider not found: ${error.provider}`,
}),
}),
),
),
)
)
.handle("workspace.destroy", (ctx) =>
workspace.destroy(ctx.params.workspaceID).pipe(
Effect.mapError(
(error) =>
new UnknownError({
message:
error._tag === "WorkspaceDriver.ProviderNotFound"
? `Workspace provider not found: ${error.provider}`
: (error.message ?? "Workspace provider failed to destroy the workspace"),
}),
),
),
)
}),
)
+51
View File
@@ -1,6 +1,8 @@
import { expect } from "bun:test"
import { createServer, type Server } from "node:http"
import { makeMemoryDriver } from "@opencode-ai/core/environment/index"
import { Workspace } from "@opencode-ai/core/workspace"
import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver"
import { Effect } from "effect"
import { it } from "../../core/test/lib/effect"
import { ServerFetch } from "../src/fetch"
@@ -34,6 +36,13 @@ const connectOpenAI = (handler: Handler) =>
),
)
const workspaceDriver = WorkspaceDriver.make({
create: ({ workspaceID }) => Effect.succeed({ binding: { workspaceID } }),
connect: () => Effect.succeed(makeMemoryDriver()),
suspendForIdle: () => Effect.void,
destroy: () => Effect.void,
})
it.live("serves the HttpApi and enforces Basic auth like the Node server", () =>
Effect.gen(function* () {
const handler = yield* ServerFetch.make({ ...options, password: "secret" })
@@ -155,6 +164,48 @@ it.live("treats destroying a missing workspace as success", () =>
}).pipe(Effect.scoped),
)
it.live("creates idempotent caller-identified workspaces through the HttpApi", () =>
Effect.gen(function* () {
const handler = yield* ServerFetch.make(options, {
overrides: [
[WorkspaceDriver.node, WorkspaceDriver.registryNode({ fake: workspaceDriver, other: workspaceDriver })],
],
})
const id = Workspace.ID.create()
const create = (body: unknown) =>
Effect.promise(() =>
handler(
new Request("http://opencode.local/api/workspace", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
),
)
const supplied = yield* create({ id, provider: "fake" })
expect(supplied.status).toBe(200)
expect(yield* Effect.promise(() => supplied.json())).toEqual({ data: id })
const repeated = yield* create({ id, provider: "fake" })
expect(repeated.status).toBe(200)
expect(yield* Effect.promise(() => repeated.json())).toEqual({ data: id })
const conflict = yield* create({ id, provider: "other" })
expect(conflict.status).toBe(409)
expect(yield* Effect.promise(() => conflict.json())).toMatchObject({
_tag: "ConflictError",
resource: id,
})
expect((yield* create({ id: "invalid", provider: "fake" })).status).toBe(400)
const minted = yield* create({ provider: "fake" })
expect(minted.status).toBe(200)
expect(yield* Effect.promise(() => minted.json())).toMatchObject({ data: expect.stringMatching(/^wrk_/) })
}).pipe(Effect.scoped),
)
it.live("serves the session view operation and missing-session error", () =>
Effect.gen(function* () {
const handler = yield* ServerFetch.make(options)