* fix(build): exec native tool binaries directly in runBuildTool #8858 routed every resolved local bin through process.execPath to avoid Windows .cmd shims — but esbuild >=0.25 ships bin/esbuild as the NATIVE platform executable (ELF on Linux), so Node parsed machine code as JS and build:cli died with 'SyntaxError: Invalid or unexpected token', turning dast-smoke red for every PR. runBuildTool now sniffs the entry's magic bytes (ELF / Mach-O / PE) and execs native binaries directly; JS entries keep going through this Node binary (the .cmd-shim avoidance #8858 wanted). Validation (RED->GREEN on this box): - RED: node node_modules/esbuild/bin/esbuild --version -> SyntaxError (ELF) - GREEN: the exact failing CI step reproduced via the new logic bundles open-sse/mcp-server/server.ts successfully (4.2MB output, 1.3s). * fix(docs): add MDX frontmatter to the 20 remaining docs without it Same failure class as AGENTROUTER_WAF (#9503) and DOCKER_RELEASE_CHANNELS (this run's dast-smoke red): any doc without frontmatter breaks the fumadocs MDX loader during next build, killing build:cli/dast-smoke for every PR. Swept ALL of docs/ (i18n mirrors excluded) in one pass so this class cannot recur one file at a time. * docs(env): document OMNIROUTE_INTERNAL_SERVICE_TOKEN(+_FILE), OPENROUTER_PROVIDER_STATS_* and embedded-Redis binding vars Pre-existing env/docs contract drift from recently merged features made check:env-doc-sync red for any docs-touching PR. Values and defaults read from the defining modules (internalServiceAuth.ts, openrouterProviderStats.ts). * fix(build): resolve bundled npm-cli.js in the standard Unix layout + safe npm fallback off-Windows The opencode-plugin step hard-failed on GitHub runners because resolveBundledNpmEntry only looked next to the node binary (Windows zip layout); hostedtoolcache Node keeps npm at <prefix>/lib/node_modules/npm. Added that candidate, and when neither exists on non-Windows the step now falls back to plain 'npm' — the .cmd-shim hazard #8858 avoids is Windows-only. * test(mutation): register xai-agent-tools-passthrough.test.ts in stryker tap.testFiles The test landed on release/v3.8.50 covering open-sse/handlers/chatCore/passthroughHelpers.ts without the stryker registration, so Fast Quality Gates' drift detection reds any PR that carries it. Mechanical registration so its mutant kills count. --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
3.6 KiB
title, version, lastUpdated
| title | version | lastUpdated |
|---|---|---|
| Proxy Port Clash Investigation | 3.8.50 | 2026-08-06 |
Proxy Port Clash Investigation
Summary
There is no port clash in the proxy auto-select / proxyFallback / proxyEgress system. The proxy subsystem uses pre-assigned registry ports — it never binds to TCP ports directly. The real EADDRINUSE history is in the process supervisor layer, where the server's main listen port can clash during crash-loop restarts.
Proxy Subsystem: No Port Binding
| Module | What It Does |
|---|---|
proxyAutoSelector.ts |
Selects a proxy config from the DB by applying health scores and rotation groups |
proxyFallback.ts |
Implements retry/fallback strategies when a selected proxy fails (try another proxy, then direct) |
proxyEgress.ts |
Probes/propagates egress IP info for logging — uses HTTP echo, not port binding |
proxyDispatcher.ts |
Creates undici.ProxyAgent dispatchers — these are HTTP-level (forward proxy), not TCP listen sockets |
proxyFetch.ts |
Patched global fetch that applies proxy dispatchers at the undici level |
None of these modules call net.createServer(), http.createServer(), or app.listen().
Port management is entirely within the request life cycle — undici manages the TCP
connection pool internally.
Fallback flow (from proxyFetch.ts runWithProxyContext):
- Try assigned proxy → proxy dispatcher
- If unreachable → direct fallback (no dispatcher)
- If still failing → error propagated up
No port allocation or release happens in this flow.
Real EADDRINUSE Root Cause: Crash-Loop Restart Race
The actual port clash was in the process supervisor (bin/cli/runtime/):
| File | Role |
|---|---|
processSupervisor.mjs |
ServerSupervisor — spawns a child process, monitors exit code, restarts |
supervisorPolicy.mjs |
waitUntilPortFree(), isPortFree(), restart policy constants |
Root cause: When the server child process crashed and was immediately restarted, the
OS had not yet released the listen socket (TIME_WAIT / TCP lingering). The restart
attempt would bind to the same port and immediately fail with EADDRINUSE, causing
another crash → another restart → exhausted restart budget → gateway dead.
Fix (#4425, in supervisorPolicy.mjs):
- Added
isPortFree(port)— attempts anet.createServer().listen()on the target port; resolvesfalseif EADDRINUSE. - Added
waitUntilPortFree(port, timeoutMs=10000, intervalMs=250)— polls every 250ms for up to 10s until the port is free, then allows the restart. - Bumped
RESTART_RESET_MSfrom 30s → 60s — the crash window was too short, causing rapid cascading restarts inside the window. - Bumped
DEFAULT_MAX_RESTARTSfrom 2 → 3 — more headroom for transient failures.
The writePidFile() / killAllSubprocesses() / cleanupPidFile() utilities in
bin/cli/utils/pid.mjs ensure clean PID file lifecycle.
Related: Live-Dashboard EADDRINUSE (#6324)
A parallel fix (live-ws-eaddrinuse-6324.test.ts) ensures startLiveDashboardServer()
rejects with a proper EADDRINUSE error (instead of an unhandled socket 'error' event
that would crash the process). The dashboard server uses a separate port from the main
API server, so when both are configured on the same port, the second bind fails
gracefully.
Current State
| Risk | Status | Remaining |
|---|---|---|
| Supervisor restart EADDRINUSE | Fixed (#4425) | None |
| LiveWS port clash | Fixed (#6324) | None |
| Proxy selection port clash | Never applicable | None |
| Two Redis CLIENT factories bind no TCP ports | Never applicable | None |
No further action needed on port clash.