426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
474 lines
15 KiB
TypeScript
474 lines
15 KiB
TypeScript
/**
|
|
* Measuring harness for chat message search at scale (#13534, follow-up #9955).
|
|
*
|
|
* Seeds a deterministic ≥10,000-message corpus into a REAL PGlite store (the
|
|
* same `@elizaos/plugin-sql` adapter + migrations that install the
|
|
* `eliza_message_search_document` FTS + `pg_trgm` GIN indexes production uses),
|
|
* then runs a labeled gold set of edge-case queries through the real
|
|
* `IDatabaseAdapter.searchMessages` and measures retrieval quality
|
|
* (precision@10 / recall@10 / MRR / nDCG@10) and query latency (p50/p95/max),
|
|
* plus index build time. No metric is fabricated: a value is `measured:true`
|
|
* only when a real query produced it; the whole run exits non-zero when a
|
|
* measured budget regresses, so CI gates on it directly.
|
|
*
|
|
* Run under bun so the real @elizaos/plugin-sql PGlite path resolves:
|
|
* bun --conditions=eliza-source packages/benchmarks/searchbench/searchbench-kpi.ts
|
|
* `run-all.mjs` is the orchestrator that spawns this and writes the dashboard.
|
|
*/
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import type { IDatabaseAdapter, UUID } from "@elizaos/core";
|
|
import { sql } from "drizzle-orm";
|
|
import { v4 } from "uuid";
|
|
// Import the plugin-sql adapter + FTS migration objects from the repo's own
|
|
// source tree (this file benchmarks the current checkout). A relative path
|
|
// pins the measured code to this tree; the bare `@elizaos/plugin-sql` specifier
|
|
// can resolve to an installed copy in a shared-node_modules worktree.
|
|
import {
|
|
createDatabaseAdapter,
|
|
DatabaseMigrationService,
|
|
memoryTable,
|
|
plugin as sqlPlugin,
|
|
} from "../../../plugins/plugin-sql/src/index.node.ts";
|
|
import { loadBudgets, quantile, recordResult, round } from "./lib.mjs";
|
|
import { ndcgAtK } from "./metric-schema.mjs";
|
|
|
|
const CORPUS_SIZE = Number(process.env.SEARCHBENCH_CORPUS ?? 10_000);
|
|
const LATENCY_REPEATS = Number(process.env.SEARCHBENCH_LATENCY_REPEATS ?? 5);
|
|
const NOW = new Date().toISOString();
|
|
|
|
/** Deterministic xorshift PRNG so corpus + placement are reproducible. */
|
|
function makeRng(seed: number): () => number {
|
|
let s = seed >>> 0 || 1;
|
|
return () => {
|
|
s ^= s << 13;
|
|
s ^= s >>> 17;
|
|
s ^= s << 5;
|
|
return ((s >>> 0) % 1_000_000) / 1_000_000;
|
|
};
|
|
}
|
|
|
|
interface GoldCase {
|
|
id: string;
|
|
kind: string;
|
|
query: string;
|
|
/** The exact message texts (and optional attachment) planted as relevant. */
|
|
relevant: Array<{
|
|
text: string;
|
|
attachment?: { title?: string; url?: string };
|
|
}>;
|
|
/** Force placement of the first relevant row at the oldest corpus index. */
|
|
oldestFirst?: boolean;
|
|
}
|
|
|
|
// Each gold case uses a rare, unique token so filler noise can never
|
|
// accidentally satisfy it — recall/precision then isolate the search behaviour,
|
|
// not lexical luck. Tokens end in the corpus-reserved 'q' suffix that filler
|
|
// never emits.
|
|
const GOLD: GoldCase[] = [
|
|
{
|
|
id: "multiword-nonadjacent",
|
|
kind: "multiword",
|
|
query: "deployq agentq",
|
|
relevant: [
|
|
{ text: "please deployq the very first agentq to the cloud now" },
|
|
{ text: "deployq your production agentq before the demo" },
|
|
],
|
|
},
|
|
{
|
|
id: "partial-word-trigram",
|
|
kind: "partial",
|
|
query: "zephyrconfig",
|
|
relevant: [
|
|
{ text: "the zephyrconfiguration file needs an edit" },
|
|
{ text: "reload the zephyrconfiguration after saving" },
|
|
],
|
|
},
|
|
{
|
|
id: "accent-fold",
|
|
kind: "accent",
|
|
query: "cafeq",
|
|
relevant: [{ text: "the caféq downtown roasts its own beans" }],
|
|
},
|
|
{
|
|
id: "quoted-phrase",
|
|
kind: "phrase",
|
|
query: '"alphaq betaq"',
|
|
relevant: [{ text: "the alphaq betaq gammaq sequence is stable" }],
|
|
// A non-adjacent distractor that a bare-OR query would wrongly rank in.
|
|
},
|
|
{
|
|
id: "url-substring",
|
|
kind: "url",
|
|
query: "exampleq.com/pathq",
|
|
relevant: [
|
|
{ text: "open https://exampleq.com/pathq/to/thing for the writeup" },
|
|
],
|
|
},
|
|
{
|
|
id: "emoji",
|
|
kind: "emoji",
|
|
query: "🛰️queryemoji",
|
|
relevant: [{ text: "launch status 🛰️queryemoji nominal today" }],
|
|
},
|
|
{
|
|
id: "cjk",
|
|
kind: "cjk",
|
|
query: "北京q",
|
|
relevant: [{ text: "会议在 北京q 举行" }],
|
|
},
|
|
{
|
|
id: "attachment-filename",
|
|
kind: "attachment",
|
|
query: "reportq-2026",
|
|
relevant: [
|
|
{
|
|
text: "here is the document you requested",
|
|
attachment: {
|
|
title: "reportq-2026-final.pdf",
|
|
url: "https://cdn.example.com/reportq-2026-final.pdf",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "near-duplicate",
|
|
kind: "duplicate",
|
|
query: "dupqmarker",
|
|
relevant: [
|
|
{ text: "dupqmarker identical body" },
|
|
{ text: "dupqmarker identical body" },
|
|
{ text: "dupqmarker identical body" },
|
|
],
|
|
},
|
|
{
|
|
id: "older-than-window",
|
|
kind: "older-hit",
|
|
query: "xyzzyneedleq",
|
|
relevant: [{ text: "the oldest message carries the xyzzyneedleq token" }],
|
|
oldestFirst: true,
|
|
},
|
|
];
|
|
|
|
interface SeededRow {
|
|
text: string;
|
|
attachment?: { title?: string; url?: string };
|
|
goldId?: string;
|
|
}
|
|
|
|
/** Build the full corpus: gold-relevant rows scattered into deterministic filler. */
|
|
function buildCorpus(): {
|
|
rows: SeededRow[];
|
|
relevantIndexByGold: Map<string, number[]>;
|
|
} {
|
|
const rng = makeRng(0xc0ffee);
|
|
const rows: SeededRow[] = new Array(CORPUS_SIZE);
|
|
const relevantIndexByGold = new Map<string, number[]>();
|
|
|
|
const goldRows: SeededRow[] = [];
|
|
let oldestGold: SeededRow | undefined;
|
|
for (const g of GOLD) {
|
|
relevantIndexByGold.set(g.id, []);
|
|
g.relevant.forEach((r, i) => {
|
|
const row: SeededRow = {
|
|
text: r.text,
|
|
attachment: r.attachment,
|
|
goldId: g.id,
|
|
};
|
|
if (g.oldestFirst && i === 0) oldestGold = row;
|
|
else goldRows.push(row);
|
|
});
|
|
}
|
|
|
|
// Fill with deterministic filler that never contains a gold token.
|
|
for (let i = 0; i < CORPUS_SIZE; i++) {
|
|
rows[i] = { text: `filler chatter ${i} ordinary words variant ${i % 11}` };
|
|
}
|
|
// The oldest-hit relevant row is pinned at index 0 (oldest timestamp).
|
|
let cursor = 0;
|
|
if (oldestGold) {
|
|
rows[0] = oldestGold;
|
|
relevantIndexByGold.get(oldestGold.goldId as string)?.push(0);
|
|
cursor = 1;
|
|
}
|
|
// Scatter the remaining gold rows across deterministic positions in the tail.
|
|
for (const row of goldRows) {
|
|
let idx = cursor + Math.floor(rng() * (CORPUS_SIZE - cursor));
|
|
while (rows[idx].goldId)
|
|
idx = cursor + Math.floor(rng() * (CORPUS_SIZE - cursor));
|
|
rows[idx] = row;
|
|
relevantIndexByGold.get(row.goldId as string)?.push(idx);
|
|
}
|
|
return { rows, relevantIndexByGold };
|
|
}
|
|
|
|
async function main(): Promise<number> {
|
|
const budgets = loadBudgets();
|
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "searchbench-"));
|
|
const agentId = v4() as UUID;
|
|
const worldId = v4() as UUID;
|
|
const entityId = v4() as UUID;
|
|
const roomId = v4() as UUID;
|
|
|
|
let adapter: IDatabaseAdapter | undefined;
|
|
try {
|
|
adapter = createDatabaseAdapter({ dataDir }, agentId);
|
|
await adapter.init();
|
|
const db = (
|
|
adapter as unknown as { getDatabase: () => never }
|
|
).getDatabase();
|
|
|
|
const migrationService = new DatabaseMigrationService();
|
|
await migrationService.initializeWithDatabase(db);
|
|
migrationService.discoverAndRegisterPluginSchemas([sqlPlugin]);
|
|
await migrationService.runAllPluginMigrations();
|
|
|
|
await adapter.createAgent({
|
|
id: agentId,
|
|
name: "searchbench",
|
|
bio: [],
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
} as never);
|
|
await adapter.createWorld({
|
|
id: worldId,
|
|
agentId,
|
|
name: "W",
|
|
serverId: "searchbench",
|
|
} as never);
|
|
await adapter.createEntities([
|
|
{ id: entityId, agentId, names: ["seed"] } as never,
|
|
]);
|
|
await adapter.createRooms([
|
|
{
|
|
id: roomId,
|
|
agentId,
|
|
worldId,
|
|
name: "r",
|
|
source: "bench",
|
|
type: "DM",
|
|
} as never,
|
|
]);
|
|
await adapter.addParticipant(entityId, roomId);
|
|
|
|
const { rows, relevantIndexByGold } = buildCorpus();
|
|
const base = Date.now() - CORPUS_SIZE * 1000;
|
|
const insertStart = Date.now();
|
|
const CHUNK = 2000;
|
|
let batch: Array<Record<string, unknown>> = [];
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const r = rows[i];
|
|
const content: Record<string, unknown> = { text: r.text };
|
|
if (r.attachment) content.attachments = [r.attachment];
|
|
batch.push({
|
|
id: v4() as UUID,
|
|
type: "messages",
|
|
content,
|
|
entityId,
|
|
agentId,
|
|
roomId,
|
|
worldId,
|
|
unique: false,
|
|
metadata: { type: "messages" },
|
|
createdAt: new Date(base + i * 1000),
|
|
});
|
|
if (batch.length >= CHUNK) {
|
|
await db.insert(memoryTable).values(batch as never);
|
|
batch = [];
|
|
}
|
|
}
|
|
if (batch.length) await db.insert(memoryTable).values(batch as never);
|
|
const insertMs = Date.now() - insertStart;
|
|
|
|
// Rebuild the FTS GIN index on the full corpus to measure index build time
|
|
// (the incremental per-insert maintenance is not a representative number).
|
|
const reindexStart = Date.now();
|
|
await db.execute(sql`REINDEX INDEX idx_memories_message_fts`);
|
|
const indexBuildMs = Date.now() - reindexStart;
|
|
|
|
// Confirm the corpus actually landed — a short corpus invalidates the run.
|
|
const countRows = (await db.execute(
|
|
sql`SELECT count(*)::int AS n FROM memories WHERE type = 'messages'`,
|
|
)) as unknown as { rows?: Array<{ n: number }> };
|
|
const corpusSize =
|
|
countRows.rows?.[0]?.n ??
|
|
(Array.isArray(countRows) ? (countRows[0] as { n: number }).n : 0);
|
|
|
|
// Which latency budget applies: with pg_trgm the substring/partial fallback
|
|
// is a trigram-GIN index scan (fast, production/cloud); without it the
|
|
// fallback scans the STORED document column (slower, this PGlite build).
|
|
const trigramRows = (await db.execute(
|
|
sql`SELECT 1 AS ok FROM pg_extension WHERE extname = 'pg_trgm'`,
|
|
)) as unknown as { rows?: unknown[] };
|
|
const trigramAvailable = Array.isArray(trigramRows.rows)
|
|
? trigramRows.rows.length > 0
|
|
: Array.isArray(trigramRows) && trigramRows.length > 0;
|
|
|
|
// Warm the plan cache once so first-query JIT/parse cost is excluded.
|
|
await adapter.searchMessages({
|
|
roomIds: [roomId],
|
|
query: "warmup",
|
|
limit: 10,
|
|
});
|
|
|
|
const goldResults: Array<Record<string, unknown>> = [];
|
|
const allLatencies: number[] = [];
|
|
for (const g of GOLD) {
|
|
const relevantIdxs = relevantIndexByGold.get(g.id) ?? [];
|
|
const relevantTexts = new Set(g.relevant.map((r) => r.text));
|
|
let last: Awaited<ReturnType<IDatabaseAdapter["searchMessages"]>> = [];
|
|
for (let rep = 0; rep < LATENCY_REPEATS; rep++) {
|
|
const t0 = performance.now();
|
|
last = await adapter.searchMessages({
|
|
roomIds: [roomId],
|
|
query: g.query,
|
|
limit: 20,
|
|
});
|
|
const dt = performance.now() - t0;
|
|
allLatencies.push(dt);
|
|
}
|
|
const returnedTexts = last.map(
|
|
(h) => (h.memory.content as { text?: string }).text ?? "",
|
|
);
|
|
const top10Flags = returnedTexts
|
|
.slice(0, 10)
|
|
.map((t) => relevantTexts.has(t));
|
|
const hitsAt10 = top10Flags.filter(Boolean).length;
|
|
const relevant = relevantIdxs.length;
|
|
const firstRank = returnedTexts.findIndex((t) => relevantTexts.has(t));
|
|
goldResults.push({
|
|
id: g.id,
|
|
query: g.query,
|
|
kind: g.kind,
|
|
relevant,
|
|
returned: last.length,
|
|
hitsAt10,
|
|
recallAt10: relevant === 0 ? null : round(hitsAt10 / relevant),
|
|
precisionAt10:
|
|
last.length === 0 ? 0 : round(hitsAt10 / Math.min(10, last.length)),
|
|
reciprocalRank: firstRank < 0 ? 0 : round(1 / (firstRank + 1)),
|
|
ndcgAt10: round(ndcgAtK(top10Flags, 10)),
|
|
latencyMs: round(allLatencies[allLatencies.length - 1], 3),
|
|
});
|
|
}
|
|
|
|
const measuredGold = goldResults.filter((r) => r.relevant !== 0);
|
|
const mean = (key: string) =>
|
|
measuredGold.length === 0
|
|
? null
|
|
: round(
|
|
measuredGold.reduce((a, r) => a + (r[key] as number), 0) /
|
|
measuredGold.length,
|
|
);
|
|
const aggregate = {
|
|
corpusSize,
|
|
trigramAvailable,
|
|
insertMs,
|
|
indexBuildMs,
|
|
precisionAt10: mean("precisionAt10"),
|
|
recallAt10: mean("recallAt10"),
|
|
mrr: mean("reciprocalRank"),
|
|
ndcgAt10: mean("ndcgAt10"),
|
|
p50LatencyMs: round(quantile(allLatencies, 0.5), 3),
|
|
p95LatencyMs: round(quantile(allLatencies, 0.95), 3),
|
|
maxLatencyMs: round(Math.max(...allLatencies), 3),
|
|
latencySamples: allLatencies.length,
|
|
};
|
|
|
|
const b = budgets.budgets;
|
|
const p95Budget = trigramAvailable
|
|
? b.maxP95LatencyMsTrigram
|
|
: b.maxP95LatencyMsNoTrigram;
|
|
const checks = [
|
|
{
|
|
name: "corpusSize",
|
|
value: aggregate.corpusSize,
|
|
budget: b.minCorpus,
|
|
cmp: "min",
|
|
},
|
|
{
|
|
name: "recallAt10",
|
|
value: aggregate.recallAt10,
|
|
budget: b.minRecallAt10,
|
|
cmp: "min",
|
|
},
|
|
{
|
|
name: "precisionAt10",
|
|
value: aggregate.precisionAt10,
|
|
budget: b.minPrecisionAt10,
|
|
cmp: "min",
|
|
},
|
|
{ name: "mrr", value: aggregate.mrr, budget: b.minMrr, cmp: "min" },
|
|
{
|
|
name: "ndcgAt10",
|
|
value: aggregate.ndcgAt10,
|
|
budget: b.minNdcgAt10,
|
|
cmp: "min",
|
|
},
|
|
{
|
|
name: trigramAvailable
|
|
? "p95LatencyMs (trigram)"
|
|
: "p95LatencyMs (no-trigram)",
|
|
value: aggregate.p95LatencyMs,
|
|
budget: p95Budget,
|
|
cmp: "max",
|
|
},
|
|
{
|
|
name: "indexBuildMs",
|
|
value: aggregate.indexBuildMs,
|
|
budget: b.maxIndexBuildMs,
|
|
cmp: "max",
|
|
},
|
|
].map((c) => ({
|
|
...c,
|
|
pass:
|
|
c.value == null
|
|
? false
|
|
: c.cmp === "min"
|
|
? c.value >= c.budget
|
|
: c.value <= c.budget,
|
|
}));
|
|
|
|
const measured = corpusSize >= (budgets.budgets.minCorpus ?? 10_000);
|
|
const allPass = measured && checks.every((c) => c.pass);
|
|
recordResult(
|
|
"searchbench",
|
|
{
|
|
measured,
|
|
summary: { corpus: corpusSize, gold: GOLD.length, ...aggregate },
|
|
aggregate,
|
|
gold: goldResults,
|
|
checks,
|
|
},
|
|
NOW,
|
|
);
|
|
|
|
for (const c of checks) {
|
|
const cmp = c.cmp === "min" ? "≥" : "≤";
|
|
console.log(
|
|
`[searchbench] ${c.pass ? "PASS" : "FAIL"} ${c.name}: ${c.value} ${cmp} ${c.budget}`,
|
|
);
|
|
}
|
|
return allPass ? 0 : 1;
|
|
} catch (err) {
|
|
// error-policy:J1 harness boundary — a genuine environment failure (PGlite
|
|
// unavailable, import failure) exits 2 (nothing measurable), never a false
|
|
// green. A budget regression is the exit-1 path above.
|
|
console.error(
|
|
`[searchbench] harness failed to measure: ${err instanceof Error ? err.stack : String(err)}`,
|
|
);
|
|
recordResult("searchbench", { measured: false, error: String(err) }, NOW);
|
|
return 2;
|
|
} finally {
|
|
try {
|
|
await (adapter as unknown as { close?: () => Promise<void> })?.close?.();
|
|
} catch {
|
|
// error-policy:J6 best-effort teardown of the scratch PGlite store.
|
|
}
|
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
main().then((code) => process.exit(code));
|