Dashboard › sentry › Session 0uyjYAMK90GY
0uyjYAMK90GYMSjDbYou are reviewing 4 PRs on BYK/loreai. Review each critically for bugs, semantic issues, edge cases, and description accuracy.
PR #602 — fix(gateway): polyfill getSystemErrorMap for Bun to prevent Sentry SDK crash
packages/gateway/instrument.ts and find the polyfill section (uses require("node:util")){} properly isolate the variable? Could require fail in the esbuild CJS bundle? Is the polyfill safe for Node.js (where the function already exists)?PR #603 — fix(core): guard against postMessage on terminated embedding worker
packages/core/src/embedding.ts — find the on("error"), on("exit"), embed(), and shutdown() methodsthis.worker in error/exit handlers race with ongoing embed() calls? Are updateWorkerRef() calls still correct after nulling worker? Does try/catch in embed() properly clean up the pending request? Does shutdown() handle the already-exited case correctly?PR #604 — fix(core): recognize WASM fatal error wrapper prefix in isWasmFatalError()
packages/core/src/embedding-worker-types.ts — find isWasmFatalError()packages/core/src/embedding-worker.ts — find the "WASM fatal error" stringPR #605 — fix(gateway): handle SSE response from upstream when stream: false was sent
packages/gateway/src/pipeline.ts — find accumulateNonStreamResponse() and extractJSONFromSSE()packages/gateway/src/llm-adapter.ts — find buildOpenAIWorkerRequest() and the SSE guard in the worker response pathdata: payloads? What if there are multiple data: lines that together form the response? What about providers that send incremental SSE chunks even in "non-streaming" mode? Is the stream: false addition safe for Anthropic workers? ...I'll start by reading all the relevant files across the 4 PRs in parallel.
diff --git a/packages/gateway/instrument.ts b/packages/gateway/instrument.ts
index ad86611..7eefca3 100644
--- a/packages/gateway/instrument.ts
+++ b/packages/gateway/instrument.ts
@@ -27,6 +27,21 @@
import { setMaxListeners } from "node:events";
setMaxListeners(15);
+// Bun doesn't implement node:util's getSystemErrorMap(). The Sentry SDK calls
+// it during processEvent() to enrich errors with system error code names. When
+// it's missing, Sentry itself crashes with "getSystemErrorMap is not a function"
+// (LOREAI-GATEWAY-1X). Provide a no-op stub so Sentry's event pipeline doesn't
+// break — the only loss is cosmetic (no OS error code labels on events).
+// Use globalThis-based patching because esbuild treats ESM namespace imports
+// as immutable and rejects property assignment on `import * as util`.
+{
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const util = require("node:util") as Record<string, unknown>;
+ if (typeof util.getSystemErrorMap ...
diff --git a/packages/core/src/embedding.ts b/packages/core/src/embedding.ts
index abeb020..560e37c 100644
--- a/packages/core/src/embedding.ts
+++ b/packages/core/src/embedding.ts
@@ -405,15 +405,18 @@ class LocalProvider implements EmbeddingProvider {
});
// Worker crash / exit — reject all in-flight requests.
+ // Null out `this.worker` in both handlers so the `?.` optional chaining
+ // in embed() prevents postMessage on a terminated Worker (LOREAI-GATEWAY-1T).
this.worker.on("error", (err: Error) => {
this.workerInitError = err.message;
this.workerReady = false;
+ this.worker = null;
+ this.initPromise = null;
log.error("embedding worker crashed:", err);
for (const [, p] of this.pendingRequests) {
p.reject(new LocalProviderUnavailableError(err));
}
this.pendingRequests.clear();
- this.updateWorkerRef();
});
this.worker.on("exit", (code) => {
@@ ...
diff --git a/packages/core/src/embedding-worker-types.ts b/packages/core/src/embedding-worker-types.ts
index 03b1c76..348ebf8 100644
--- a/packages/core/src/embedding-worker-types.ts
+++ b/packages/core/src/embedding-worker-types.ts
@@ -103,6 +103,11 @@ export function isOomError(msg: string): boolean {
* thread's `on("exit")` handler marks the provider as broken.
*/
export function isWasmFatalError(msg: string): boolean {
+ // Recognize the wrapper prefix the worker adds before process.exit(1).
+ // The main thread receives "WASM fatal error (worker exiting): <raw>"
+ // and must classify it as fatal to create LocalProviderUnavailableError
+ // instead of a plain Error in the on("message") handler.
+ if (/WASM fatal error/i.test(msg)) return true;
// WASM abort() — "Aborted(). Build with -sASSERTIONS for more info."
if (/\bAborted\b/i.test(msg)) return true;
// RuntimeError from WASM (e.g. "unreachable", "memory access out of bounds")
diff --git a/packages/gateway/src/llm-adapter.ts b/packages/gateway/src/llm-adapter.ts
index d58bc26..fdd60c5 100644
--- a/packages/gateway/src/llm-adapter.ts
+++ b/packages/gateway/src/llm-adapter.ts
@@ -332,6 +332,7 @@ function buildOpenAIWorkerRequest(
body: JSON.stringify({
model: model.modelID,
max_completion_tokens: maxTokens,
+ stream: false,
...(temperature != null && { temperature }),
messages,
}),
@@ -521,13 +522,31 @@ export function createGatewayLLMClient(
finalStatus = response.status;
if (response.ok) {
- const rawData = await response.json();
+ // Guard: some providers return SSE even when stream: false
+ // was sent. Extract JSON from the data: lines instead.
+ const ct = response.headers.get("content-type") ?? "";
+ let rawData: unknown;
+ if (ct.includes("text/event-stream")) {
+ const ...
{"body":"## Summary\nPrevents Sentry SDK crash under Bun by polyfilling `node:util.getSystemErrorMap()`.\n\n## Changes\n- Adds a no-op stub (`() => new Map()`) for `getSystemErrorMap` in `instrument.ts` before `Sentry.init()`\n- The polyfill only activates when the function is missing (Bun); Node.js is unaffected\n\n## Sentry Issue\nCloses LOREAI-GATEWAY-1X: `TypeError: ile.getSystemErrorMap is not a function`\n\nThe crash occurs inside Sentry's own `processEvent()` pipeline when running under Bun. Since `@sentry/bun` depends on `@sentry/node-core` which calls `getSystemErrorMap`, switching SDK packages doesn't help — only a polyfill works.\n\n## Verification\n- `pnpm -r typecheck` — all packages pass\n- `pnpm run lint` — no new warnings/errors","title":"fix(gateway): polyfill getSystemErrorMap for Bun to prevent Sentry SDK crash"}
{"body":"## Summary\nPrevents `InvalidStateError: Worker has been terminated` by properly nulling the worker reference on crash/exit and adding try/catch guards around `postMessage()` calls.\n\n## Changes\n- Null `this.worker` in both `on('error')` and `on('exit')` handlers so the `?.` optional chaining in `embed()` prevents posting to a dead worker\n- Reset `this.initPromise = null` in both handlers so future `ensureWorker()` calls can re-create the worker after broken state is cleared\n- Wrap `postMessage()` in `embed()` with try/catch — rejects with `LocalProviderUnavailableError` so callers degrade gracefully\n- Wrap `postMessage()` in `shutdown()` with try/catch — resolves immediately if the worker already exited\n- Removed now-unnecessary `updateWorkerRef()` calls from error/exit handlers (worker is null)\n\n## Root Cause\nRace condition: when the worker calls `process.exit(1)` on WASM fatal error, the `on('exit')` handler fires asynchronously. ...
{"body":"## Summary\nFixes a classification gap where the main thread's `isWasmFatalError()` didn't recognize its own wrapper prefix, causing ONNX OOM errors to be reported as plain `Error` instead of `LocalProviderUnavailableError`.\n\n## Changes\n- Adds `/WASM fatal error/i` regex pattern to `isWasmFatalError()` in `embedding-worker-types.ts`\n- This matches the wrapper prefix `\"WASM fatal error (worker exiting): <raw>\"` that the worker adds before `process.exit(1)`\n\n## Root Cause\nThe embedding worker wraps raw OOM errors in a descriptive prefix before posting to the main thread. The main thread's `isWasmFatalError()` checked for `Aborted`, `RuntimeError`, and `isOomError()` — but the wrapped string no longer matched any of these (the raw numeric code is no longer bare, and the prefix doesn't contain those keywords). ...
{"body":"## Summary\nHandles the case where upstream providers return SSE-formatted responses despite `stream: false` being sent, preventing a `SyntaxError` from `response.json()`.\n\n## Changes\n- Adds content-type check in `accumulateNonStreamResponse()` (pipeline.ts) — detects `text/event-stream` and extracts JSON from `data:` lines via new `extractJSONFromSSE()` helper\n- Adds the same SSE guard in the worker LLM adapter response path (llm-adapter.ts)\n- Adds explicit `stream: false` to `buildOpenAIWorkerRequest()` body — it was previously omitted, which some providers interpret as defaulting to streaming\n\n## Root Cause\nDeepSeek (and potentially other OpenAI-compatible providers) returns SSE-formatted responses even when `stream: false` is sent. The gateway's non-streaming path called `response.json()` without checking the content-type, causing `SyntaxError: Unexpected token 'd', \"data: {\"id\"...` when the body started with `data: ` SSE lines.\n\nThe recall follow-up path ...
Now let me read the full source files to understand the broader context around each change.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/instrument.ts</path> <type>file</type> <content> 1: /** 2: * Sentry instrumentation. 3: * 4: * By default, Sentry is enabled in production builds (where esbuild 5: * injects a real semver string via LORE_CLI_VERSION) and disabled in 6: * dev mode (VERSION falls back to "dev"). 7: * 8: * Explicit control via SENTRY_ENABLED env var: 9: * - SENTRY_ENABLED=1 → force on (useful for local dev testing) 10: * - SENTRY_ENABLED=0 → force off (opt out in production) 11: * 12: * When force-enabled in dev mode, environment is set to "development"; 13: * production builds always use "production". 14: * 15: * This file is imported as a side-effect from both entry points: 16: * - src/cli/bin.ts (standalone binary) 17: * - src/index.ts (npm bundle / direct execution) 18: * 19: * Static imports are used (not dynamic) because the CJS npm bundle 20: * ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/core/src/embedding.ts</path>
<type>file</type>
<content>
1: /**
2: * Embedding integration for vector search.
3: *
4: * Supports multiple embedding providers behind a common interface:
5: * - "local" (default): @huggingface/transformers + nomic-embed-text-v1.5
6: * (768 dims, Matryoshka-capable). Runs ONNX inference in a worker thread.
7: * - "voyage": Voyage AI API (voyage-code-3, 1024 dims)
8: * - "openai": OpenAI API (text-embedding-3-small, 1536 dims)
9: *
10: * Provides embedding generation, pure-JS cosine similarity, and vector search
11: * over the knowledge and distillation tables. All operations are gated behind
12: * `search.embeddings.enabled` config + the provider's API key env var — falls
13: * back silently to FTS-only when unavailable.
14: */
15:
16: import { db } from "./db";
17: import { config } from "./config";
18: import * as log ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/core/src/embedding-worker-types.ts</path> <type>file</type> <content> 1: /** 2: * Shared message types for the embedding worker thread. 3: * 4: * The embedding worker (`embedding-worker.ts`) runs ONNX inference via 5: * `@huggingface/transformers` in a separate `node:worker_threads` Worker 6: * so the main thread's event loop stays free during inference. This file 7: * defines the message protocol between the main thread (`LocalProvider` 8: * in `embedding.ts`) and the worker. 9: * 10: * Imported by both sides — keep this file free of runtime dependencies. 11: */ 12: 13: // --------------------------------------------------------------------------- 14: // Main thread → Worker 15: // --------------------------------------------------------------------------- 16: 17: /** Request an embedding batch. ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/core/src/embedding-worker.ts</path> <type>file</type> <content> 1: /** 2: * Embedding worker thread — runs ONNX inference via @huggingface/transformers 3: * off the main thread. 4: * 5: * This file is the entry point for a `node:worker_threads` Worker spawned by 6: * `LocalProvider` in `embedding.ts`. It owns the transformers.js pipeline 7: * and processes embed requests sequentially from a priority queue. Moving 8: * inference here keeps the main thread's event loop free — HTTP requests, 9: * SSE streams, and session APIs are no longer blocked during embedding. 10: * 11: * Communication uses `parentPort` message passing with structured clone. 12: * Float32Array vectors are sent back directly (Bun preserves identity). 13: * 14: * The worker applies Nomic's recommended post-processing: 15: * 1. Mean pooling (via pipeline option) 16: * 2. ...
Now let me read the pipeline.ts and llm-adapter.ts files for the PR #605 context:
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
2180: }
2181: },
2182: });
2183:
2184: return new Response(stream, {
2185: status: 200,
2186: headers: {
2187: "content-type": "text/event-stream",
2188: "cache-control": "no-cache",
2189: connection: "keep-alive",
2190: },
2191: });
2192: }
2193:
2194: /**
2195: * Accumulate a non-streaming upstream response into a GatewayResponse. ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
300: "Content-Type": "application/json",
301: "anthropic-version": "2023-06-01",
302: ...authHeaders(cred),
303: ...oauthHeaders,
304: },
305: body,
306: };
307: }
308:
309: /**
310: * Build OpenAI Chat Completions API request.
311: * Returns the full URL, headers, and serialized body.
312: */
313: function buildOpenAIWorkerRequest(
314: target: ProviderTarget,
315: cred: AuthCredential,
316: model: { providerID: string; modelID: string },
317: system: string,
318: user: string,
319: maxTokens: number,
320: temperature?: number,
321: ): { url: string; headers: Record<string, string>; body: string } {
322: const messages: Array<{ role: string; content: string }> = [];
323: if (system) messages.push({ role: "system", content: system });
324: messages.push({ ...
Now let me check the current state (before the PRs) of the files to understand what the diffs change, and look for any SSE-related handling elsewhere:
Found 18 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/pipeline.ts:
Line 2044: // JSON parsing is unused on the streaming path (assertSSEResponse
Line 2187: "content-type": "text/event-stream",
Line 2567: "content-type": "text/event-stream",
Line 3609: upstreamResponse.headers.get("content-type") ?? "text/event-stream",
Line 3629: "content-type": "text/event-stream",
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/translate/openai.ts:
Line 464: "content-type": "text/event-stream",
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/translate/openai-responses.ts:
Line 744: "content-type": ...
Found 2 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/llm-adapter.ts:
Line 342: function parseAnthropicResponse(data: {
Line 530: : parseAnthropicResponse(rawData);
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
580: log.info(
581: `worker auth error ${response.status}, credential refreshed — retrying: ${text.slice(0, 200)}`,
582: );
583: req =
584: target.protocol === "openai"
585: ? buildOpenAIWorkerRequest(
586: target,
587: freshCred,
588: model,
589: system,
590: user,
591: maxTokens,
592: opts?.temperature,
593: )
594: : buildAnthropicWorkerRequest(
595: target,
596: freshCred,
597: model,
598: ...
Let me also check the recall.ts SSE handling for comparison, and the buildAnthropicWorkerRequest to verify stream parameter handling:
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/recall.ts</path> <type>file</type> <content> 500: } 501: 502: // --------------------------------------------------------------------------- 503: // Content-type guards — fail loud on a stream-flag / consumer mismatch 504: // --------------------------------------------------------------------------- 505: 506: /** 507: * Assert an upstream recall follow-up response is SSE (`text/event-stream`). 508: * 509: * The streaming follow-up path consumes the body via `parseSSEStream()`. If 510: * the follow-up's `stream` flag is ever wrong, the upstream returns JSON and 511: * the SSE parser silently yields zero events — the client gets the recall 512: * marker then dead air. Throwing here converts that silent failure into a 513: * loud, greppable error (caught by the recall try/catch → marker fallback + 514: * Sentry via log.error). ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
240: * Build Anthropic Messages API request.
241: * Returns the full URL, headers, and serialized body.
242: */
243: function buildAnthropicWorkerRequest(
244: target: ProviderTarget,
245: cred: AuthCredential,
246: model: { providerID: string; modelID: string },
247: system: string,
248: user: string,
249: maxTokens: number,
250: sessionID?: string,
251: temperature?: number,
252: ): { url: string; headers: Record<string, string>; body: string } {
253: // For bearer tokens (Claude Code OAuth), inject the billing header
254: // as the first system block with a cch=00000 placeholder that gets
255: // signed after JSON serialization.
256: const billingBlock =
257: cred.scheme === "bearer" ? ...
Now I have a thorough understanding of all four PRs. Let me also check one more thing about the stream: false for Anthropic:
No files found
No files found
Good — neither buildAnthropicWorkerRequest nor buildOpenAIWorkerRequest currently sends a stream parameter. The PR adds stream: false only to the OpenAI worker request. Let me verify the updateWorkerRef behavior and check the on("error") → on("exit") sequencing for PR #603:
Found 7 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/core/src/embedding.ts:
Line 356: this.updateWorkerRef();
Line 365: this.updateWorkerRef();
Line 401: this.updateWorkerRef();
Line 416: this.updateWorkerRef();
Line 439: this.updateWorkerRef();
Line 454: private updateWorkerRef(): void {
Line 486: this.updateWorkerRef();
No files found
Now I have all the context I need. Let me compile the comprehensive review.
fix(gateway): polyfill getSystemErrorMap for Bun to prevent Sentry SDK crashThe polyfill at instrument.ts:37-43 uses a bare block {} to scope a require("node:util") call, checks if getSystemErrorMap is missing, and patches it with () => new Map().
None.
None.
Double-defense overlap with beforeSend filter. Line 141 already has /getSystemErrorMap/ in TRANSIENT_ERROR_PATTERNS, which drops these events in beforeSend. With the polyfill, that pattern should never match anymore. The filter entry is now dead code — harmless but slightly misleading. Consider adding a comment that the filter is belt-and-suspenders for the polyfill, or removing it.
require in ESM context — comment says why, but the eslint-disable is narrow. The comment explains why import * as util won't work (esbuild treats ESM namespace as immutable). The approach is correct — require("node:util") returns the live module object whose properties can be mutated. In the esbuild CJS bundle, require is a real CommonJS require. In dev ESM mode, Node.js provides require via the ESM loader's synthetic require. Both paths work. The block scope {} correctly isolates the const util variable. All good.
Return type of stub. () => new Map() returns Map<any, any>. The real getSystemErrorMap() returns Map<number, string>. This is fine since the Sentry SDK only iterates the map looking for a code — an empty map just yields no enrichment. Cosmetic loss only, as the PR description states.
Approve. Clean, minimal, well-scoped fix. The polyfill is properly guarded (typeof !== "function"), safe on Node.js (function already exists → no-op), and the block scope prevents variable leaks. ...