From c6b308a1c839871e5bfb32f36678d2a44ced2a77 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 19 Apr 2026 08:34:09 +0800 Subject: [PATCH] fix(eval): teardown PGLite engines so bun run eval:run exits 0 The multi-adapter runner left PGLite engines alive after each run. GbrainAfterAdapter and HybridNoGraphAdapter both instantiate a PGLiteEngine in init() but never disconnect it; Bun's shutdown path exits with code 99 when embedded-Postgres workers outlive main(). Added optional `teardown?(state)` to the Adapter interface, implemented it on both engine-backed adapters, and call it from scoreOneRun after the N=5 loop. ripgrep-bm25 and vector-only hold no DB resources and don't need a teardown. Verified: gbrain-after, hybrid-nograph, ripgrep-bm25, vector-only all exit 0 at N=1. Full test:eval passes (57 tests). No metric change. --- eval/runner/adapters/hybrid-nograph.ts | 5 +++++ eval/runner/multi-adapter.ts | 6 ++++++ eval/runner/types.ts | 8 ++++++++ 3 files changed, 19 insertions(+) diff --git a/eval/runner/adapters/hybrid-nograph.ts b/eval/runner/adapters/hybrid-nograph.ts index 5c99a36e7..be5cde0eb 100644 --- a/eval/runner/adapters/hybrid-nograph.ts +++ b/eval/runner/adapters/hybrid-nograph.ts @@ -76,6 +76,11 @@ export class HybridNoGraphAdapter implements Adapter { return { engine } satisfies HybridNoGraphState; } + async teardown(state: BrainState): Promise { + const s = state as HybridNoGraphState; + await s.engine.disconnect(); + } + /** Build a markdown string importFromContent can parse. * Format: YAML frontmatter then body; matches what gbrain import expects. */ private buildContentMarkdown(p: Page): string { diff --git a/eval/runner/multi-adapter.ts b/eval/runner/multi-adapter.ts index 8247614ee..4aa4782c3 100644 --- a/eval/runner/multi-adapter.ts +++ b/eval/runner/multi-adapter.ts @@ -217,6 +217,11 @@ class GbrainAfterAdapter implements Adapter { rank: i + 1, })); } + + async teardown(state: unknown): Promise { + const { engine } = state as { engine: PGLiteEngine }; + await engine.disconnect(); + } } /** @@ -340,6 +345,7 @@ async function scoreOneRun( for (const r of topK) if (relevant.has(r.page_id)) totalCorrect++; totalExpected += relevant.size; } + if (adapter.teardown) await adapter.teardown(state); return { mean_precision_at_k: queries.length > 0 ? totalP / queries.length : 0, mean_recall_at_k: queries.length > 0 ? totalR / queries.length : 0, diff --git a/eval/runner/types.ts b/eval/runner/types.ts index af53fc81c..36a3694d2 100644 --- a/eval/runner/types.ts +++ b/eval/runner/types.ts @@ -179,6 +179,14 @@ export interface Adapter { * Adapters that don't have a poison path return an empty map. */ getPoisonDisposition?(state: BrainState): Record; + + /** + * Release any resources held by `state` (DB connections, file locks, + * worker threads). Called once per run after scoring completes. + * Adapters that hold no resources can omit this. Without it, PGLite-backed + * adapters leak engine workers and Bun exits 99 at the end of the run. + */ + teardown?(state: BrainState): Promise; } // ─── Scorer helpers ─────────────────────────────────────────────────