fix(core): surface workspace wake failures as spawn errors
This commit is contained in:
@@ -4,6 +4,7 @@ import { Workspace } from "@opencode-ai/schema/workspace"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Clock, Context, Duration, Effect, Exit, Layer, Ref, Schedule, Schema, Scope } from "effect"
|
||||
import { systemError } from "effect/PlatformError"
|
||||
import { make as makeSpawner } from "effect/unstable/process/ChildProcessSpawner"
|
||||
import type { Driver as EnvironmentDriver } from "./environment/driver"
|
||||
import { Database } from "./database/database"
|
||||
@@ -169,7 +170,17 @@ const layer = (options: Options) =>
|
||||
Effect.acquireRelease(
|
||||
locks.withLock(workspaceID)(
|
||||
Effect.gen(function* () {
|
||||
const connection = yield* open(workspaceID).pipe(Effect.orDie)
|
||||
const connection = yield* open(workspaceID).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
systemError({
|
||||
_tag: "Unknown",
|
||||
module: "Workspace",
|
||||
method: "spawn",
|
||||
description: `Failed to wake workspace ${workspaceID}`,
|
||||
cause,
|
||||
}),
|
||||
),
|
||||
)
|
||||
yield* Ref.set(connection.lastActivity, yield* Clock.currentTimeMillis)
|
||||
yield* Ref.update(connection.active, (active) => active + 1)
|
||||
return connection
|
||||
@@ -184,6 +195,7 @@ const layer = (options: Options) =>
|
||||
),
|
||||
).pipe(Effect.flatMap((connection) => connection.environment.spawner.spawn(command))),
|
||||
)
|
||||
// Overrides are connection-bound; route them per spawn before any workspace driver ships them.
|
||||
return { spawner, overrides: initial.environment.overrides }
|
||||
}),
|
||||
destroy: Effect.fn("Workspace.destroy")(function* (workspaceID) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { testEffect } from "./lib/effect"
|
||||
|
||||
const calls: Array<{ readonly operation: string; readonly binding?: WorkspaceDriver.Binding }> = []
|
||||
const memory = makeMemoryDriver()
|
||||
let failConnect = false
|
||||
|
||||
const driver = WorkspaceDriver.make({
|
||||
create: ({ workspaceID }) => {
|
||||
@@ -23,6 +24,7 @@ const driver = WorkspaceDriver.make({
|
||||
},
|
||||
connect: ({ binding }) => {
|
||||
calls.push({ operation: "connect", binding })
|
||||
if (failConnect) return Effect.fail(new WorkspaceDriver.Error({ message: "wake failed" }))
|
||||
return Effect.succeed(memory)
|
||||
},
|
||||
suspendForIdle: ({ binding, saveBinding }) => {
|
||||
@@ -51,7 +53,10 @@ const it = testEffect(
|
||||
),
|
||||
)
|
||||
|
||||
beforeEach(() => calls.splice(0))
|
||||
beforeEach(() => {
|
||||
calls.splice(0)
|
||||
failConnect = false
|
||||
})
|
||||
|
||||
it.effect("persists the workspace lifecycle and reconnects after idle suspension", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -97,3 +102,25 @@ it.effect("roundtrips nullable bindings through the workspace table", () =>
|
||||
expect(row).toEqual({ id, provider: "fake", binding: null, created_at: 10, last_used_at: 20 })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("surfaces wake failures through the spawn error channel", () =>
|
||||
Effect.gen(function* () {
|
||||
const workspace = yield* Workspace.Service
|
||||
const created = yield* workspace.create("fake")
|
||||
const environment = yield* workspace.connect(created.id)
|
||||
|
||||
yield* TestClock.adjust("6 minutes")
|
||||
failConnect = true
|
||||
|
||||
const error = yield* Effect.scoped(environment.spawner.spawn(ChildProcess.make("wake"))).pipe(Effect.flip)
|
||||
expect(error).toMatchObject({
|
||||
_tag: "PlatformError",
|
||||
reason: {
|
||||
_tag: "Unknown",
|
||||
module: "Workspace",
|
||||
method: "spawn",
|
||||
description: `Failed to wake workspace ${created.id}`,
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user