Skip to content

Commit 1e00c59

Browse files
author
bcode
committed
fix(laminar): drain OTel processors before process.exit() to stop losing final agent spans
Eval traces were missing the final LLM span on ~27% of runs (gemini-3-flash 76%, glm-5.1 34%, mimo 14%, gpt-5.5 1%, claude 0%). Root cause is a process-exit race: the plugin event hook in packages/opencode/src/plugin/index.ts:249 is invoked fire-and-forget (`void hook["event"]?.(...)`), so the bcode-laminar `session.idle` handler's `processor.forceFlush()` Promise is discarded, and the unconditional `process.exit()` in the top-level `finally` (index.ts:252) kills in-flight gRPC exports. Model-dependence comes from emit shape: tool-only-then-final-text models (glm-5.1, gemini-3-flash) make one extra tool-less LLM round at the end whose lone `ai.streamText.doStream` span ends 50-200ms before idle and is the freshest unflushed thing in the BatchSpanProcessor queue at exit. Tool-call+ text-in-same-step models (claude-opus, gpt-5.5) fold the final answer into a step that ended seconds earlier and was already flushed in a prior batch. Fix: in the top-level `finally` of index.ts, before `process.exit()`, fetch the global OTel TracerProvider via `@opentelemetry/api`, duck-check `forceFlush`, and race it against a 3 s timeout so a wedged exporter cannot hang bcode on exit. Generic to any OTel-based plugin. Does not touch the deeper bug (the fire-and-forget `event` hook); that is the proper upstream fix to anomalyco/opencode.
1 parent 90424e2 commit 1e00c59

1 file changed

Lines changed: 15 additions & 0 deletions

File tree

packages/opencode/src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { PluginCommand } from "./cli/cmd/plug"
4545
import { Heap } from "./cli/heap"
4646
import { drizzle } from "drizzle-orm/bun-sqlite"
4747
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
48+
import { trace } from "@opentelemetry/api"
4849

4950
const processMetadata = ensureProcessMetadata("main")
5051

@@ -245,6 +246,20 @@ try {
245246
}
246247
process.exitCode = 1
247248
} finally {
249+
// Drain any registered OTel span processors (e.g. bcode-laminar) before
250+
// exiting. The plugin's `session.idle` event handler is invoked
251+
// fire-and-forget (`packages/opencode/src/plugin/index.ts:249`), so its
252+
// `processor.forceFlush()` Promise was never awaited — without this drain,
253+
// `process.exit()` kills any in-flight gRPC export and the final agent
254+
// span is lost. Bounded with a 3 s race so a wedged exporter cannot hang
255+
// bcode on exit. Generic to any OTel-based plugin, not laminar-specific.
256+
const provider = trace.getTracerProvider() as { forceFlush?: () => Promise<void> }
257+
if (provider.forceFlush) {
258+
await Promise.race([
259+
provider.forceFlush().catch(() => {}),
260+
new Promise<void>((resolve) => setTimeout(resolve, 3000)),
261+
])
262+
}
248263
// Some subprocesses don't react properly to SIGTERM and similar signals.
249264
// Most notably, some docker-container-based MCP servers don't handle such signals unless
250265
// run using `docker run --init`.

0 commit comments

Comments
 (0)