From a1cdf15b758971a142293fc58ca79ed80e3bc106 Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Thu, 25 Jun 2026 11:06:16 -0700 Subject: [PATCH] atlas: disable keep-alive on the test/proxy LLM client to fix Premature close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Atlas LLM seam (OpenAIDistiller) is exercised in tests against an in-process aimock server via the OpenAI SDK, whose node-fetch transport pools keep-alive sockets. Under the full suite — every aimock-backed test file runs its own server in parallel, doubled by the built dist copy — a server can close an idle pooled socket exactly as a request reuses it, which node-fetch surfaces as 'FetchError: ... Premature close'. This made all aimock-backed suites (atlas-llm, atlas-exclude, atlas-adapter-episodic, atlas-artifact-sync, ...) fail under load: flaky locally, deterministic in CI (64 tests, incl. the 9 atlas-artifact-sync failures). When a baseURL is set (in this repo only ever a local aimock/proxy), pin a non-keep-alive http/https Agent so each request uses a fresh socket, removing the reuse race. Production (no baseURL -> real OpenAI over HTTPS) keeps the SDK default keep-alive agent. --- src/atlas/llm.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/atlas/llm.ts b/src/atlas/llm.ts index d48d3b5..14c16df 100644 --- a/src/atlas/llm.ts +++ b/src/atlas/llm.ts @@ -18,6 +18,8 @@ // the typed shapes below. import OpenAI from "openai"; +import { Agent as HttpAgent } from "node:http"; +import { Agent as HttpsAgent } from "node:https"; import type { CandidateFragment, Classification } from "./types.js"; import { mostRestrictiveSensitivity } from "./types.js"; @@ -301,9 +303,26 @@ export class OpenAIDistiller implements LlmDistiller { "mock server for tests.", ); } + // When a baseURL is configured it is, in this repo, only ever a local + // aimock/proxy server (see the comment above). Under the full test suite + // every aimock-backed file runs its own in-process server in parallel + // (src + the built dist copy, so ~2x the servers), and the OpenAI SDK's + // node-fetch pools keep-alive sockets. Under that contention a server can + // close an idle pooled socket exactly as a request reuses it, which + // node-fetch surfaces as `FetchError: ... Premature close` — a flaky, + // load-dependent failure across every aimock test (deterministic in CI). + // Pinning a non-keep-alive agent for the mock/proxy case forces a fresh + // socket per request, removing the reuse race. Production (no baseURL → + // real OpenAI over HTTPS) keeps the SDK's default keep-alive agent. + const httpAgent = baseURL + ? baseURL.startsWith("https:") + ? new HttpsAgent({ keepAlive: false }) + : new HttpAgent({ keepAlive: false }) + : undefined; this.client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}), + ...(httpAgent ? { httpAgent } : {}), }); } }