Skip to content

Commit 7700f77

Browse files
committed
fix: avoid automation startup layer cycle
1 parent d55a7f6 commit 7700f77

8 files changed

Lines changed: 66 additions & 12 deletions

File tree

packages/opencode/src/automation/automation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1538,7 +1538,7 @@ export const defaultLayer = Layer.suspend(() =>
15381538
Layer.provide(SessionPrompt.defaultLayer),
15391539
Layer.provide(SessionSummary.defaultLayer),
15401540
Layer.provide(Provider.defaultLayer),
1541-
Layer.provide(Worktree.defaultLayer),
1541+
Layer.provide(Worktree.appLayer),
15421542
),
15431543
)
15441544

packages/opencode/src/cli/effect-cmd.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Argv } from "yargs"
22
import { Effect, Schema } from "effect"
33
import { AppRuntime, type AppServices } from "@/effect/app-runtime"
4+
import { Automation } from "@/automation/automation"
45
import { InstanceStore } from "@/project/instance-store"
56
import { InstanceRef } from "@/effect/instance-ref"
67
import { Instance } from "@/project/instance"
@@ -94,7 +95,16 @@ export const effectCmd = <Args, A>(opts: EffectCmdOpts<Args, A>) =>
9495
)
9596
try {
9697
await Instance.restore(ctx, () =>
97-
AppRuntime.runPromise(opts.handler(args).pipe(Effect.provideService(InstanceRef, ctx))),
98+
AppRuntime.runPromise(
99+
Effect.gen(function* () {
100+
yield* Automation.Service.use((automation) =>
101+
automation
102+
.init()
103+
.pipe(Effect.catchCause((cause) => Effect.logWarning("automation init failed", { cause }))),
104+
)
105+
return yield* opts.handler(args)
106+
}).pipe(Effect.provideService(InstanceRef, ctx)),
107+
),
98108
)
99109
} finally {
100110
await AppRuntime.runPromise(store.dispose(ctx))

packages/opencode/src/project/bootstrap.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Plugin } from "../plugin"
22
import { Format } from "../format"
3-
import { Automation } from "@/automation/automation"
43
import { LSP } from "@/lsp/lsp"
54
import { File } from "../file"
65
import { Snapshot } from "../snapshot"
@@ -24,7 +23,6 @@ export const layer = Layer.effect(
2423
// Yield each bootstrap dep at layer init so `run` itself has R = never.
2524
// InstanceStore imports only the lightweight tag from bootstrap-service.ts,
2625
// so it can depend on bootstrap without importing this implementation graph.
27-
const automation = yield* Automation.Service
2826
const config = yield* Config.Service
2927
const file = yield* File.Service
3028
const fileWatcher = yield* FileWatcher.Service
@@ -47,7 +45,7 @@ export const layer = Layer.effect(
4745
// Each service self-manages its own slow work via Effect.forkScoped against
4846
// its per-instance state scope. We just await materialization here.
4947
yield* Effect.forEach(
50-
[automation, reference, lsp, shareNext, format, file, fileWatcher, vcs, snapshot, project],
48+
[reference, lsp, shareNext, format, file, fileWatcher, vcs, snapshot, project],
5149
(s) => s.init().pipe(Effect.catchCause((cause) => Effect.logWarning("init failed", { cause }))),
5250
{ concurrency: "unbounded", discard: true },
5351
).pipe(Effect.withSpan("InstanceBootstrap.init"))
@@ -59,7 +57,6 @@ export const layer = Layer.effect(
5957

6058
export const defaultLayer: Layer.Layer<Service> = layer.pipe(
6159
Layer.provide([
62-
Automation.defaultLayer,
6360
Bus.layer,
6461
Config.defaultLayer,
6562
File.defaultLayer,
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { Automation } from "@/automation/automation"
2+
import { InstanceRef } from "@/effect/instance-ref"
3+
import { Effect } from "effect"
14
import { AppRuntime } from "@/effect/app-runtime"
25
import { context } from "./instance-context"
36
import { InstanceStore } from "./instance-store"
@@ -6,7 +9,17 @@ export async function provide<R>(input: { directory: string; fn: () => R }): Pro
69
const ctx = await AppRuntime.runPromise(
710
InstanceStore.Service.use((store) => store.load({ directory: input.directory })),
811
)
9-
return context.provide(ctx, () => input.fn())
12+
return context.provide(ctx, async () => {
13+
await AppRuntime.runPromise(
14+
Automation.Service.use((automation) =>
15+
automation.init().pipe(
16+
Effect.catchCause((cause) => Effect.logWarning("automation init failed", { cause })),
17+
Effect.provideService(InstanceRef, ctx),
18+
),
19+
),
20+
)
21+
return input.fn()
22+
})
1023
}
1124

1225
export * as WithInstance from "./with-instance"

packages/opencode/src/server/routes/instance/httpapi/middleware/instance-context.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Automation } from "@/automation/automation"
12
import { WorkspaceRef } from "@/effect/instance-ref"
23
import { InstanceStore } from "@/project/instance-store"
34
import { Effect, Layer } from "effect"
@@ -23,12 +24,18 @@ function decode(input: string): string {
2324
function provideInstanceContext<E>(
2425
effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
2526
store: InstanceStore.Interface,
27+
automation: Automation.Interface,
2628
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
2729
return Effect.gen(function* () {
2830
const route = yield* WorkspaceRouteContext
2931
return yield* store.provide(
3032
{ directory: decode(route.directory) },
31-
effect.pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
33+
Effect.gen(function* () {
34+
yield* automation
35+
.init()
36+
.pipe(Effect.catchCause((cause) => Effect.logWarning("automation init failed", { cause })))
37+
return yield* effect
38+
}).pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
3239
)
3340
})
3441
}
@@ -37,13 +44,15 @@ export const instanceContextLayer = Layer.effect(
3744
InstanceContextMiddleware,
3845
Effect.gen(function* () {
3946
const store = yield* InstanceStore.Service
40-
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store))
47+
const automation = yield* Automation.Service
48+
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store, automation))
4149
}),
4250
)
4351

4452
export const instanceRouterMiddleware = HttpRouter.middleware()(
4553
Effect.gen(function* () {
4654
const store = yield* InstanceStore.Service
47-
return (effect) => provideInstanceContext(effect, store)
55+
const automation = yield* Automation.Service
56+
return (effect) => provideInstanceContext(effect, store, automation)
4857
}),
4958
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Effect, Layer } from "effect"
2+
import { Automation } from "../../src/automation/automation"
3+
4+
export const noopAutomationLayer = Layer.succeed(
5+
Automation.Service,
6+
Automation.Service.of({
7+
init: () => Effect.void,
8+
list: () => Effect.succeed([]),
9+
get: () => Effect.die("unused automation test service"),
10+
create: () => Effect.die("unused automation test service"),
11+
update: () => Effect.die("unused automation test service"),
12+
remove: () => Effect.die("unused automation test service"),
13+
duplicate: () => Effect.die("unused automation test service"),
14+
runNow: () => Effect.die("unused automation test service"),
15+
listRuns: () => Effect.succeed([]),
16+
getRun: () => Effect.die("unused automation test service"),
17+
listFindings: () => Effect.succeed([]),
18+
diff: () => Effect.succeed([]),
19+
markRunRead: () => Effect.die("unused automation test service"),
20+
archiveRun: () => Effect.die("unused automation test service"),
21+
cancelRun: () => Effect.die("unused automation test service"),
22+
}),
23+
)

packages/opencode/test/server/httpapi-instance-context.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { workspaceRouterMiddleware } from "../../src/server/routes/instance/http
2222
import { resetDatabase } from "../fixture/db"
2323
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
2424
import { withFixedWorkspaceID } from "../fixture/flag"
25+
import { noopAutomationLayer } from "../fixture/automation"
2526
import { waitGlobalBusEvent } from "./global-bus"
2627
import { testEffect } from "../lib/effect"
2728

@@ -58,7 +59,7 @@ const it = testEffect(
5859

5960
const instanceContextTestLayer = instanceRouterMiddleware
6061
.combine(workspaceRouterMiddleware)
61-
.layer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal))
62+
.layer.pipe(Layer.provide(noopAutomationLayer), Layer.provide(Socket.layerWebSocketConstructorGlobal))
6263

6364
const localAdapter = (directory: string): WorkspaceAdapter => ({
6465
name: "Local Test",

packages/opencode/test/server/httpapi-promptasync-context.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Project } from "../../src/project/project"
2626
import { instanceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/instance-context"
2727
import { workspaceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/workspace-routing"
2828
import { resetDatabase } from "../fixture/db"
29+
import { noopAutomationLayer } from "../fixture/automation"
2930
import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture"
3031
import { testEffect } from "../lib/effect"
3132

@@ -62,7 +63,7 @@ const it = testEffect(
6263

6364
const instanceContextTestLayer = instanceRouterMiddleware
6465
.combine(workspaceRouterMiddleware)
65-
.layer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal))
66+
.layer.pipe(Layer.provide(noopAutomationLayer), Layer.provide(Socket.layerWebSocketConstructorGlobal))
6667

6768
const localAdapter = (directory: string): WorkspaceAdapter => ({
6869
name: "Local Test",

0 commit comments

Comments
 (0)