1import * as Cloudflare from "alchemy/Cloudflare";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as HttpClientRequest from "effect/http/HttpClientRequest";
5import { OpencodeContainer } from "./opencode-container.ts";
6
7// Matches OPENCODE_SERVER_PASSWORD=bench in contexts/opencode/Dockerfile
8// (username defaults to `opencode`): base64("opencode:bench").
9const AUTH = "Basic b3BlbmNvZGU6YmVuY2g=";
10
11/**
12 * Durable Object backing one opencode container instance. `boot()` blocks
13 * until opencode's `/global/health` endpoint answers `{"healthy":true}` AND a
14 * session can be created — the same two-request readiness probe the MicroVM
15 * hosts use, so the platforms share one definition of "usable coding agent".
16 * NOTE: the authoritative cold-start clock runs in the Worker AROUND the whole
17 * DO call — the container layer eagerly starts the container during DO
18 * construction, so a clock started here would miss part of the start.
19 */
20export class OpencodeObject extends Cloudflare.DurableObject<OpencodeObject>()(
21 "BenchOpencodeObject",
22 Effect.gen(function* () {
23 const container = yield* OpencodeContainer;
24
25 return Effect.gen(function* () {
26 const { fetch } = yield* container.getTcpPort(8080);
27
28 return {
29 boot: () =>
30 Effect.gen(function* () {
31 const start = yield* Effect.sync(() => Date.now());
32 yield* Effect.gen(function* () {
33 const health = yield* fetch(
34 HttpClientRequest.get("http://container/global/health").pipe(
35 HttpClientRequest.setHeader("authorization", AUTH),
36 ),
37 );
38 const healthBody = yield* health.text;
39 if (
40 health.status !== 200 ||
41 !healthBody.includes('"healthy":true')
42 ) {
43 return yield* Effect.fail(
44 new Error(
45 `opencode health ${health.status}: ${healthBody.slice(0, 120)}`,
46 ),
47 );
48 }
49 // A real write through the app — proves the server is
50 // functional, not merely listening.
51 const session = yield* fetch(
52 HttpClientRequest.post("http://container/session").pipe(
53 HttpClientRequest.setHeader("authorization", AUTH),
54 HttpClientRequest.bodyJsonUnsafe({}),
55 ),
56 );
57 const sessionBody = yield* session.text;
58 if (session.status !== 200 || !sessionBody.includes('"id"')) {
59 return yield* Effect.fail(
60 new Error(
61 `opencode session ${session.status}: ${sessionBody.slice(0, 120)}`,
62 ),
63 );
64 }
65 }).pipe(
66 Effect.retry({
67 schedule: Schedule.min([Schedule.exponential("1 second"), Schedule.spaced("5 seconds")]),
68 times: 40,
69 }),
70 );
71 const readyMs = (yield* Effect.sync(() => Date.now())) - start;
72 return { readyMs };
73 }),
74 shutdown: () => container.destroy().pipe(Effect.ignore),
75 };
76 });
77 }).pipe(
78 Effect.provide(
79 Cloudflare.Containers.layer(OpencodeContainer, {
80 enableInternet: true,
81 }),
82 ),
83 ),
84) {}