fix(opencode): flush embedded server telemetry

This commit is contained in:
Aiden Cline 2026-07-30 21:32:19 +00:00 committed by opencode-agent[bot]
commit 31d25c8f08
4 changed files with 60 additions and 26 deletions

View file

@ -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({ export const RunCommand = effectCmd({
command: "run [message..]", command: "run [message..]",
describe: "run opencode with a message", describe: "run opencode with a message",
@ -902,19 +914,12 @@ export const RunCommand = effectCmd({
if (interactive && !args.attach && !args.session && !args.continue) { if (interactive && !args.attach && !args.session && !args.continue) {
const model = pick(args.model) const model = pick(args.model)
const { runInteractiveLocalMode } = await import("./run/runtime") const { runInteractiveLocalMode } = await import("./run/runtime")
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { const server = await embeddedServer(ServerAuth.header())
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
try { try {
return await runInteractiveLocalMode({ return await runInteractiveLocalMode({
directory: directory ?? root, directory: directory ?? root,
fetch: fetchFn, fetch: server.fetch,
resolveAgent: localAgent, resolveAgent: localAgent,
session, session,
share, share,
@ -932,6 +937,8 @@ export const RunCommand = effectCmd({
}) })
} catch (error) { } catch (error) {
dieInteractive(error) dieInteractive(error)
} finally {
await server.dispose()
} }
} }
@ -940,20 +947,17 @@ export const RunCommand = effectCmd({
return await execute(sdk) return await execute(sdk)
} }
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => { const server = await embeddedServer(ServerAuth.header())
const { Server } = await import("@/server/server") try {
const request = new Request(input, init) const sdk = createOpencodeClient({
const headers = new Headers(request.headers) baseUrl: "http://opencode.internal",
const auth = ServerAuth.header() fetch: server.fetch,
if (auth) headers.set("Authorization", auth) directory,
return Server.Default().app.fetch(new Request(request, { headers })) })
}) as typeof globalThis.fetch await execute(sdk)
const sdk = createOpencodeClient({ } finally {
baseUrl: "http://opencode.internal", await server.dispose()
fetch: fetchFn, }
directory,
})
await execute(sdk)
}) })
}), }),
}) })

View file

@ -72,6 +72,7 @@ export const rpc = {
async shutdown() { async shutdown() {
await InstanceRuntime.disposeAllInstances() await InstanceRuntime.disposeAllInstances()
if (server) await server.stop(true) if (server) await server.stop(true)
if (Server.Default.loaded()) await Server.Default().dispose()
process.off("unhandledRejection", onUnhandledRejection) process.off("unhandledRejection", onUnhandledRejection)
process.off("uncaughtException", onUncaughtException) process.off("uncaughtException", onUncaughtException)
}, },

View file

@ -54,14 +54,14 @@ class ListenerServerService extends Context.Service<ListenerServerService, Liste
) {} ) {}
export const Default = lazy(() => { export const Default = lazy(() => {
const handler = HttpApiApp.webHandler().handler const web = HttpApiApp.webHandler()
const app: ServerApp = { const app: ServerApp = {
fetch: (request: Request) => handler(request, HttpApiApp.context), fetch: (request: Request) => web.handler(request, HttpApiApp.context),
request(input, init) { request(input, init) {
return app.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), 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() { export async function openapi() {

View file

@ -23,6 +23,35 @@ describe("opencode run (non-interactive subprocess)", () => {
60_000, 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( cliIt.concurrent(
"prints each completed text part in order around a tool continuation", "prints each completed text part in order around a tool continuation",
({ llm, opencode }) => ({ llm, opencode }) =>