diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 3927f615a0..3e93f9fbfa 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -123,6 +123,18 @@ async function toolError(part: ToolPart) { } } +async function embeddedServer(auth?: string) { + const { Server } = await import("@/server/server") + const server = Server.Default() + const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input, init) + const headers = new Headers(request.headers) + if (auth) headers.set("Authorization", auth) + return server.app.fetch(new Request(request, { headers })) + }) as typeof globalThis.fetch + return { fetch, dispose: server.dispose } +} + export const RunCommand = effectCmd({ command: "run [message..]", describe: "run opencode with a message", @@ -902,19 +914,12 @@ export const RunCommand = effectCmd({ if (interactive && !args.attach && !args.session && !args.continue) { const model = pick(args.model) const { runInteractiveLocalMode } = await import("./run/runtime") - const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { - const { Server } = await import("@/server/server") - const request = new Request(input, init) - const headers = new Headers(request.headers) - const auth = ServerAuth.header() - if (auth) headers.set("Authorization", auth) - return Server.Default().app.fetch(new Request(request, { headers })) - }) as typeof globalThis.fetch + const server = await embeddedServer(ServerAuth.header()) try { return await runInteractiveLocalMode({ directory: directory ?? root, - fetch: fetchFn, + fetch: server.fetch, resolveAgent: localAgent, session, share, @@ -932,6 +937,8 @@ export const RunCommand = effectCmd({ }) } catch (error) { dieInteractive(error) + } finally { + await server.dispose() } } @@ -940,20 +947,17 @@ export const RunCommand = effectCmd({ return await execute(sdk) } - const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { - const { Server } = await import("@/server/server") - const request = new Request(input, init) - const headers = new Headers(request.headers) - const auth = ServerAuth.header() - if (auth) headers.set("Authorization", auth) - return Server.Default().app.fetch(new Request(request, { headers })) - }) as typeof globalThis.fetch - const sdk = createOpencodeClient({ - baseUrl: "http://opencode.internal", - fetch: fetchFn, - directory, - }) - await execute(sdk) + const server = await embeddedServer(ServerAuth.header()) + try { + const sdk = createOpencodeClient({ + baseUrl: "http://opencode.internal", + fetch: server.fetch, + directory, + }) + await execute(sdk) + } finally { + await server.dispose() + } }) }), }) diff --git a/packages/opencode/src/cli/tui/worker.ts b/packages/opencode/src/cli/tui/worker.ts index 4cf6b2d446..818b8d7416 100644 --- a/packages/opencode/src/cli/tui/worker.ts +++ b/packages/opencode/src/cli/tui/worker.ts @@ -72,6 +72,7 @@ export const rpc = { async shutdown() { await InstanceRuntime.disposeAllInstances() if (server) await server.stop(true) + if (Server.Default.loaded()) await Server.Default().dispose() process.off("unhandledRejection", onUnhandledRejection) process.off("uncaughtException", onUncaughtException) }, diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 440b992c15..4fd937ab87 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -54,14 +54,14 @@ class ListenerServerService extends Context.Service { - const handler = HttpApiApp.webHandler().handler + const web = HttpApiApp.webHandler() const app: ServerApp = { - fetch: (request: Request) => handler(request, HttpApiApp.context), + fetch: (request: Request) => web.handler(request, HttpApiApp.context), request(input, init) { return app.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init)) }, } - return { app } + return { app, dispose: web.dispose } }) export async function openapi() { diff --git a/packages/opencode/test/cli/run/run-process.test.ts b/packages/opencode/test/cli/run/run-process.test.ts index bd5847e272..20a5971259 100644 --- a/packages/opencode/test/cli/run/run-process.test.ts +++ b/packages/opencode/test/cli/run/run-process.test.ts @@ -23,6 +23,35 @@ describe("opencode run (non-interactive subprocess)", () => { 60_000, ) + cliIt.concurrent( + "flushes server telemetry before exiting", + ({ llm, opencode }) => + Effect.gen(function* () { + const requests: string[] = [] + const collector = yield* Effect.acquireRelease( + Effect.sync(() => + Bun.serve({ + port: 0, + fetch(request) { + requests.push(new URL(request.url).pathname) + return new Response(null, { status: 200 }) + }, + }), + ), + (server) => Effect.sync(() => server.stop(true)), + ) + yield* llm.text("telemetry response") + + const result = yield* opencode.run("say hi", { + env: { OTEL_EXPORTER_OTLP_ENDPOINT: collector.url.toString().replace(/\/$/, "") }, + }) + + opencode.expectExit(result, 0) + expect(requests).toContain("/v1/traces") + }), + 60_000, + ) + cliIt.concurrent( "prints each completed text part in order around a tool continuation", ({ llm, opencode }) =>