fix(core): surface workspace wake failures as spawn errors

This commit is contained in:
Kit Langton
2026-08-07 23:30:45 -04:00
parent 2c80a77906
commit b31cec37e3
2 changed files with 41 additions and 2 deletions
+13 -1
View File
@@ -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) {
+28 -1
View File
@@ -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}`,
},
})
}),
)