DashboardsentrySession 0nrqYGUubOz0

Session 0nrqYGUubOz0

Full ID: 0nrqYGUubOz0JEXIH
Messages: 10
Distillations: 0
Time range: 2026-06-07 21:47:36 — 2026-06-07 21:48:38

Conversation (10 messages)

Investigate Sentry issue LOREAI-GATEWAY-1Z: TypeError: Failed to parse URL from http://127.0.0.1:undefined/health

Context from Sentry:

  • 1 event, fatal/unhandled
  • Release: 0.24.1
  • Runtime: node v24.13.0
  • The error comes from a test script at /tmp/test-startserver.mjs line 13
  • Breadcrumbs show: "port: undefined hosts: undefined" — so startServer() returned an object where .port and .hosts are both undefined
  • The script does: const server = startServer(config); server.port; await server.ready; — the port is undefined even AFTER await server.ready resolves

The test script:

const server = startServer(config);
console.log("port:", server.port, "hosts:", server.hosts);
await server.ready;
console.log("ready!");
const r = await fetch(`http://127.0.0.1:${server.port}/health`);

Your task:

  1. Read packages/gateway/src/server.ts — find:

    • The startServer() function
    • How it returns the server object
    • When/how .port is set on the returned object
    • The .ready promise and what it resolves to
    • Whether .port can be undefined if the server hasn't started listening yet
  2. Check if startServer() uses server.address() to get the port — in Node.js, this returns null until the server is actively listening

  3. Check if there's a race condition where ready resolves but port hasn't been set yet

  4. Look at the config object — does it require a port to be specified, or does it default to 0 (random)?

  5. Check if the port property is set synchronously vs asynchronously

Return: (1) exact code path where port becomes undefined with file:line references, (2) root cause analysis, (3) proposed fix to ensure port is always defined after ready resolves

gateway · 2026-06-07 21:47:36

I'll investigate this systematically. Let me start by reading the server file and related configuration.

claude-opus-4-6 · 2026-06-07 21:47:36 · ~36 tokens
result
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/server.ts</path>
<type>file</type>
<content>
1: /**
2:  * HTTP server for the Lore gateway proxy.
3:  *
4:  * Routes:
5:  *   POST /v1/messages            → Anthropic protocol
6:  *   POST /v1/chat/completions    → OpenAI Chat Completions protocol
7:  *   POST /v1/responses           → OpenAI Responses API protocol
8:  *   POST /v1/responses/compact   → Codex compaction (Responses API)
9:  *   POST /v1/compact             → Explicit compaction summary (Pi plugin, etc.)
10:  *   GET  /v1/models              → Passthrough to upstream
11:  *   GET  /health                 → Health check
12:  *
13:  * Uses `node:http` `createServer` with Web `Request`/`Response` — the same
14:  * code runs under both Bun and the Node.js npm distribution. ...
result
Found 9 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/server.ts:
  Line 289: export async function startServer(config: GatewayConfig): Promise<{

  Line 297:   // loadConfig() always provides these, but startServer is a public export.

  Line 524:  * Used by startServer() to surface the async bind to callers.


/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/index.ts:
  Line 7:  *   import { startServer, loadConfig } from "@loreai/gateway";

  Line 22: export { startServer } from "./server";


/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/cli/start.ts:
  Line 7: import { startServer } from "../server";

  Line 112:     // startServer() is async — it binds each host sequentially so the

  Line 114:     // returned `ready` promise is the same one startServer already

  ...
result
Found 4 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/server.ts:
  Line 306:   if (!Number.isFinite(config.port) || config.port < 0) {

  Line 434:   // Bind sequentially so the OS-assigned port (when config.port is 0)

  Line 439:   let resolvedPort = config.port;

  Line 496:         resolvedPort = addr.port;
gateway · 2026-06-07 21:47:48

Now I see the key issue. startServer() is async — it returns a Promise. Let me check how the test script and the CLI use it, and verify the config and DEFAULT_PORT.

claude-opus-4-6 · 2026-06-07 21:47:48 · ~58 tokens
result
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/config.ts</path>
<type>file</type>
<content>
1: /**
2:  * Gateway configuration — loaded from environment variables with sensible
3:  * defaults. No Zod, no file-based config — minimal @loreai/core dependency
4:  * (only `normalizeRemoteUrl` for git URL canonicalization).
5:  */
6: 
7: import {
8:   normalizeRemoteUrl,
9:   discoverWorkspaceRoot,
10:   UNATTRIBUTED_PROJECT_PREFIX,
11:   isUnattributedProjectPath,
12: } from "@loreai/core";
13: 
14: // ---------------------------------------------------------------------------
15: // Port defaults
16: // ---------------------------------------------------------------------------
17: 
18: /**
19:  * Default port preference order when LORE_LISTEN_PORT is not set. ...
result
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/cli/start.ts</path>
<type>file</type>
<content>
1: /**
2:  * `lore start` — start the gateway server without auto-launching an agent.
3:  *
4:  * Extracted from the old top-level index.ts boot logic.
5:  */
6: import { loadConfig, DEFAULT_PORTS, type GatewayConfig } from "../config";
7: import { startServer } from "../server";
8: import { resetPipelineState } from "../pipeline";
9: import { writePortFile, removePortFile } from "../portfile";
10: import { embedding } from "@loreai/core";
11: import { safeExit } from "./exit";
12: 
13: export interface StartOptions {
14:   port?: number;
15:   hosts?: string[];
16:   debug?: boolean;
17:   /** Suppress verbose banner (env vars, export hints). Used in embedded mode. */
18:   quiet?: boolean;
19:   /** Remote gateway URL. When set, `lore run` delegates to this gateway
20:    *  instead of starting a local one. ...
result
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/index.ts</path>
<type>file</type>
<content>
1: /**
2:  * Lore Gateway — package entry point.
3:  *
4:  * Library exports for programmatic use, plus `_cli()` for the CLI binary.
5:  *
6:  * Library usage:
7:  *   import { startServer, loadConfig } from "@loreai/gateway";
8:  *
9:  * CLI usage (via bin wrapper):
10:  *   lore start
11:  *   lore run claude
12:  */
13: import "../instrument";
14: import { fileURLToPath } from "node:url";
15: 
16: // ---------------------------------------------------------------------------
17: // Library API
18: // ---------------------------------------------------------------------------
19: 
20: export { loadConfig, DEFAULT_PORTS, DEFAULT_PORT } from "./config";
21: export type { GatewayConfig } from "./config";
22: export { startServer } from "./server";
23: export { handleRequest, resetPipelineState } from "./pipeline";
24: ...
gateway · 2026-06-07 21:47:55

Now I have the complete picture. Let me verify my understanding by checking the exact return type and the startGateway usage pattern one more time.

claude-opus-4-6 · 2026-06-07 21:47:55 · ~51 tokens
result
Found 10 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/recall-openai-stream.test.ts:
  Line 137:     const server = await startServer(config);


/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/helpers/harness.ts:
  Line 83:   const server = await startServer(config);


/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/api.test.ts:
  Line 41:   server = await startServer(config);


/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/script/bundle.ts:
  Line 342: export declare function startServer(config: GatewayConfig): Promise<{


/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/server.ts:
  Line 289: export async function ...
result
Found 9 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/recall-openai-stream.test.ts:
  Line 118:     const { startServer } = await import("../src/server");

  Line 137:     const server = await startServer(config);


/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/hosted-config.test.ts:
  Line 2:  * Tests for hosted mode configuration defaults in startGateway().

  Line 4:  * The override logic in startGateway() follows three-tier precedence:

  Line 15:  * Replicate the hosted mode override logic from startGateway() so we can

  Line 73:   // startGateway override logic

  Line 76:   describe("startGateway override logic", () => {


/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/api.test.ts:
  Line 29:   const { startServer } = await import("../src/server");

  ...
gateway · 2026-06-07 21:47:59

Now let me check the bundle.ts declaration to see what the published npm package exposes — this is what the test script at /tmp/test-startserver.mjs would import:

claude-opus-4-6 · 2026-06-07 21:47:59 · ~56 tokens
result
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/script/bundle.ts</path>
<type>file</type>
<content>
330: /** The primary default port (first in the fallback chain). */
331: export declare const DEFAULT_PORT: 3207;
332: 
333: /** Load Lore gateway configuration from the environment. */
334: export declare function loadConfig(): GatewayConfig;
335: 
336: /**
337:  * Start the Lore gateway server.
338:  *
339:  * Prefer startGateway() for most use cases — it handles port fallback,
340:  * port file management, and existing-instance reuse automatically.
341:  */
342: export declare function startServer(config: GatewayConfig): Promise<{
343:   stop: () => void;
344:   port: number;
345:   hosts: string[];
346:   /** Resolves when all bound servers are listening. ...
gateway · 2026-06-07 21:48:38

I now have the complete picture. Here is my analysis:


Investigation Results

1. Exact Code Path Where Port Becomes Undefined

The bug is not in startServer() itself — the function is correct internally. The bug is a caller-side misuse caused by the function's async signature being easy to miss.

startServer() signaturepackages/gateway/src/server.ts:289:

export async function startServer(config: GatewayConfig): Promise<{
  stop: () => void;
  port: number;
  hosts: string[];
  ready: Promise<void>;
}>

startServer() is an async function that returns a Promise. It does NOT return the server object synchronously.

The failing test script does:

const server = startServer(config);          // <-- NO await!
console.log("port:", server.port, "hosts:", server.hosts);  // Promise has no .port → undefined
await server.ready;                          // Promise has no .ready → undefined, await undefined resolves immediately
const r = await fetch(`http://127.0.0.1:${server.port}/health`);  // port is still undefined → boom

Here is what happens step by step:

  1. Line const server = startServer(config) — Without await, server is a Promise object, not the resolved { stop, port, hosts, ready } object.

  2. Line console.log("port:", server.port, "hosts:", server.hosts)Promise objects have no .port or .hosts properties, so both are undefined. This matches the Sentry breadcrumb exactly: "port: undefined hosts: undefined".

  3. Line await server.readyserver.ready is undefined (because server is a Promise, not the server object). await undefined resolves immediately as a no-op, so the script proceeds without waiting for the server to actually start.

  4. **Line fetch(\http://127.0.0.1:${server.port}/health`)** — server.portis stillundefined, producing the URL http://127.0.0.1:undefined/health, which causes TypeError: Failed to parse URL from ...

claude-opus-4-6 · 2026-06-07 21:48:38 · ~1.9K tokens