DashboardsentrySession 0uyjYAMK90GY

Session 0uyjYAMK90GY

Full ID: 0uyjYAMK90GYMSjDb
Messages: 16
Distillations: 0
Time range: 2026-06-07 21:01:58 — 2026-06-07 21:04:57

Conversation (16 messages)

You are reviewing 4 PRs on BYK/loreai. Review each critically for bugs, semantic issues, edge cases, and description accuracy.

PR #602fix(gateway): polyfill getSystemErrorMap for Bun to prevent Sentry SDK crash

  • Read packages/gateway/instrument.ts and find the polyfill section (uses require("node:util"))
  • Check: Does the block scope {} 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 #603fix(core): guard against postMessage on terminated embedding worker

  • Read packages/core/src/embedding.ts — find the on("error"), on("exit"), embed(), and shutdown() methods
  • Check: Does nulling this.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 #604fix(core): recognize WASM fatal error wrapper prefix in isWasmFatalError()

  • Read packages/core/src/embedding-worker-types.ts — find isWasmFatalError()
  • Read packages/core/src/embedding-worker.ts — find the "WASM fatal error" string
  • Check: Does the new regex match correctly? Could it match false positives? Is the priority order of checks correct?

PR #605fix(gateway): handle SSE response from upstream when stream: false was sent

  • Read packages/gateway/src/pipeline.ts — find accumulateNonStreamResponse() and extractJSONFromSSE()
  • Read packages/gateway/src/llm-adapter.ts — find buildOpenAIWorkerRequest() and the SSE guard in the worker response path
  • Check: Does extractJSONFromSSE correctly handle multiline data: 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? ...
gateway · 2026-06-07 21:01:58

I'll start by reading all the relevant files across the 4 PRs in parallel.

claude-opus-4-6 · 2026-06-07 21:01:58 · ~26 tokens
result
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 ...
result
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) => {
@@ ...
result
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")
result
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 ...
result
{"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"}
result
{"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. ...
result
{"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). ...
result
{"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 ...
gateway · 2026-06-07 21:02:35

Now let me read the full source files to understand the broader context around each change.

claude-opus-4-6 · 2026-06-07 21:02:35 · ~31 tokens
result
<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:  * ...
result
<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 ...
result
<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. ...
result
<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. ...
gateway · 2026-06-07 21:02:43

Now let me read the pipeline.ts and llm-adapter.ts files for the PR #605 context:

claude-opus-4-6 · 2026-06-07 21:02:43 · ~28 tokens
result
<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. ...
result
<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({ ...
gateway · 2026-06-07 21:02:53

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:

claude-opus-4-6 · 2026-06-07 21:02:53 · ~51 tokens
result
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": ...
result
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);
result
<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:         ...
gateway · 2026-06-07 21:03:01

Let me also check the recall.ts SSE handling for comparison, and the buildAnthropicWorkerRequest to verify stream parameter handling:

claude-opus-4-6 · 2026-06-07 21:03:01 · ~46 tokens
result
<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). ...
result
<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" ? ...
gateway · 2026-06-07 21:03:07

Now I have a thorough understanding of all four PRs. Let me also check one more thing about the stream: false for Anthropic:

claude-opus-4-6 · 2026-06-07 21:03:07 · ~43 tokens
result
No files found
result
No files found
gateway · 2026-06-07 21:03:13

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:

claude-opus-4-6 · 2026-06-07 21:03:13 · ~96 tokens
result
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();
result
No files found
gateway · 2026-06-07 21:04:57

Now I have all the context I need. Let me compile the comprehensive review.


PR Review: 4 PRs on BYK/loreai


PR #602 — fix(gateway): polyfill getSystemErrorMap for Bun to prevent Sentry SDK crash

Code Analysis

The 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().

(1) Critical Issues

None.

(2) Medium Issues

None.

(3) Minor Issues

  1. 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.

  2. 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.

  3. 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.

(4) Verdict

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. ...

claude-opus-4-6 · 2026-06-07 21:04:57 · ~4.8K tokens