one default port, and it is 9000

Every PORT fallback in the tree now says 9000. There were three answers to one
question, each defensible where it was written and none of them visible from the
others:

  server.tsx and 19 sidecars   5000    a default from before there was an installer
  user-instance.ts             9010    what scripts/setup-old/setup.sh really wrote
  .env.example                 9000    what we told people to write

5000 goes first because macOS binds it — AirPlay Receiver has owned it since
Monterey, so a dev server there fails to bind or gets shadowed by something that
answers.

All 22 sites moved together, which is the point. Changing the app alone would have
turned a consistent-but-wrong default into a split one: the app on 9000 while
nineteen sidecars still dialled 5000.

9010 was the interesting one. It was the only value that ever matched a real
machine, because it is what the old installer wrote — and it was in the single
file whose disagreement would have broken chat alone, with nothing else looking
wrong. Its own comment records the same bug being fixed once already, within the
file, by a change that left it disagreeing with everything outside it.

Note what these defaults actually are: the sidecars bind nothing. user-instance.ts
has no listener at all — it builds ws:// and http:// URLs that both address the
app's single listener. So every one of these numbers is a guess at where the app
is, for a value that .env always supplies. Worth removing rather than aligning,
which is a separate change.

Not typechecked (empty node_modules, frozen installs). Every edited file parses
under `bun build --no-bundle`; the pm2 profile loads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 23:38:15 +00:00
co-authored by Claude Opus 5
parent 3bc06bba3c
commit e72cae4830
24 changed files with 33 additions and 26 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ import './servers/api/chat/opencode/sidecar-server'; // subscribe to the opencod
import type { SidecarRegistration } from './servers/sidecar/registration-protocol';
import { toShellUsername } from './servers/data-path';
const { PORT = '5000' } = process.env;
const { PORT = '9000' } = process.env;
// Build static file routes from public/
const publicRoutes: Record<string, (req: Request) => Response> = {};
+1 -1
View File
@@ -23,7 +23,7 @@ import { logger } from './logger';
/** A name has to survive being typed into a prompt and into a shell, so keep it boring. */
const NAME_RE = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$|^[a-z0-9]$/;
const API_ORIGIN = `http://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_ORIGIN = `http://127.0.0.1:${process.env.PORT ?? '9000'}`;
export function registerAgentPanelRoutes(router: Hono<any>): void {
// GET /chat/agent-panels?dashboardId=… — the address book for one dashboard.
+1 -1
View File
@@ -1,6 +1,6 @@
import { sign } from '@@/jwt';
const { PORT = '5000', PUBLIC_URL } = process.env;
const { PORT = '9000', PUBLIC_URL } = process.env;
const PUBLIC_HOST = (() => {
try {
+1 -1
View File
@@ -23,7 +23,7 @@ import { DATA_PATH } from '../../data-path';
// machine-facing interface. Same split officer-email already uses.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
+1 -1
View File
@@ -3,7 +3,7 @@ import { loadState, flushAndSave, acquireLock, releaseLock, getState } from './s
import { startAnthropicProxy, getProxySecret, ensureProxySecret } from './proxy';
import { createSidecarConnector } from '../connect';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// ── Startup ──
+10 -3
View File
@@ -61,9 +61,16 @@ async function resolveOwner() {
const dbUser = await resolveOwner();
const email = dbUser.email;
// Same officer instance for both, so the fallback port has to agree. It used to default to 5000 for the
// WebSocket and 9010 for the REST base, which would have split them apart if PORT were ever unset.
const OFFICER_PORT = process.env.PORT ?? '9010';
// Both URLs address the SAME officer instance — this file binds nothing, and the app serves HTTP and
// WebSocket on one listener. So the fallback port has to agree, and twice it did not: 5000 for the
// WebSocket against 9010 for the REST base here, and then 9010 here against 5000 in the app and every
// other sidecar. The second one was the worse half, because chat is the only thing that would have
// broken and nothing else would have looked wrong.
//
// 9000 everywhere now, matching server.tsx and .env.example. 9010 was the old installer's default
// (scripts/setup-old/setup.sh), which is why it was the only one of the three that ever matched a real
// machine.
const OFFICER_PORT = process.env.PORT ?? '9000';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${OFFICER_PORT}`;
const OFFICER_API_URL = process.env.OFFICER_API_URL ?? `http://127.0.0.1:${OFFICER_PORT}`;
const MCP_SERVER_SCRIPT = resolve(import.meta.dir, '../../mcp-tool-server.ts');
+1 -1
View File
@@ -5,7 +5,7 @@ type AnyCommand = SidecarCommand;
type AnyEvent = SidecarEvent;
type SidecarConnectorConfig = {
apiUrl: string; // ws://127.0.0.1:5000/api/sidecar/register
apiUrl: string; // ws://127.0.0.1:9000/api/sidecar/register
name: string;
capabilities: string[];
onCommand: (cmd: AnyCommand, reply: (msg: AnyEvent) => void) => void;
+1 -1
View File
@@ -5,7 +5,7 @@ import { broadcastEmailNew } from './routes';
import { startEmailServer } from './http';
import { createSidecarConnector } from '../connect';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// The sidecar used to reach BACK into the platform's queue over this socket to get a sync run —
// enqueueViaWs / listJobsViaWs and a pending-response map. Syncs run in this process now
+1 -1
View File
@@ -42,7 +42,7 @@ import {
// sidecar from being a general-purpose SSRF hop into whatever else is on that host.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// Everything under /api/v1 the UI legitimately needs.
//
+1 -1
View File
@@ -54,7 +54,7 @@ import { MIN_VERSION_LABEL } from './version';
// — the mistake the Soulseek panels made with 37 raw upstream calls. Every quirk is absorbed here.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
+1 -1
View File
@@ -51,7 +51,7 @@ import { getConfig } from './upstream';
// update/*, installation/*, mail config, settings writes, ownership transfer — is deliberately unreachable.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
+1 -1
View File
@@ -43,7 +43,7 @@ import { getConfig, probe } from './upstream';
// general proxy, and why HLS forces it to keep Jellyfin's own paths.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
+1 -1
View File
@@ -22,7 +22,7 @@ import { callMemos, getMemosConfig, invalidateMemosConfig, normalizeBase, probe
// SSRF hop into whatever else is on that host.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// Everything under /api/v1 the UI legitimately needs. Auth routes are excluded on purpose: signin and
// signout would mint or destroy sessions on the instance, and this sidecar authenticates with a stored
+1 -1
View File
@@ -114,7 +114,7 @@ const asKeys = (v: unknown): string[] | null =>
// `v` = per-album version stamp; unchanged `v` ⇒ nothing changed ⇒ the phone can skip re-downloading.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// ── Audio-streaming HTTP server ──
+1 -1
View File
@@ -26,7 +26,7 @@ import type { Notification, NotifyType } from './types';
// the visible string is composed here rather than sent by the producer.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
const VALID_TYPES: NotifyType[] = ['job', 'mail', 'agent', 'download', 'test'];
+1 -1
View File
@@ -18,7 +18,7 @@ import { runOpenCodeTurnOnServe, killServeTurn, listRunningServeTurns, stopAllSe
// CRUD only, with turns spawned as `opencode run --dir <cwd>` subprocesses — that path was deleted on
// 2026-08-10 once the serve had streaming, mid-turn injection and interrupt working end to end.
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
const OPENCODE_BIN = process.env.OPENCODE_BIN || join(homedir(), '.opencode', 'bin', 'opencode');
const SERVE_CWD = join(DATA_PATH, 'opencode_server');
const HEALTH_TIMEOUT_MS = 20_000;
+1 -1
View File
@@ -46,7 +46,7 @@ import { getConfig } from './upstream';
// ETag included. The administrative half of Immich's API is unreachable — see routes.ts for the list.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
+1 -1
View File
@@ -19,7 +19,7 @@ import { startServer } from './server.mjs';
import 'dotenv/config';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
const REGISTER_URL = `${API_URL}/api/sidecar/register`;
const RECONNECT_DELAYS = [200, 500, 1000, 2000, 4000, 8000, 15000];
+1 -1
View File
@@ -37,7 +37,7 @@ import { handleOfficerRoute } from './officer';
// (src/servers/sidecar/vault/index.ts) once the client needs real-time updates. SignalR carries its
// credential as an `?access_token=` query param on the socket, so the key injection differs from HTTP.
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
+1 -1
View File
@@ -45,7 +45,7 @@ import { getTransmissionConfig } from './upstream';
// platform, and which daemon to talk to is per-owner data.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
+1 -1
View File
@@ -21,7 +21,7 @@ import { getVaultBase, getVaultWsBase, stripHopByHop, redactPath } from './upstr
// The server listens on a random loopback port, reported to the API on connect so it can route here.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
+1 -1
View File
@@ -3,7 +3,7 @@ import * as vncManager from './vnc-manager';
import { createSidecarConnector } from '../connect';
import { getOwnerHomeDir } from '@@/data-path';
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
// ── Command handlers ──
+1 -1
View File
@@ -81,7 +81,7 @@ import { invalidateAll } from './resolve';
// WALLET_LOCKED from signing paths only; every read above keeps working.
// ─────────────────────────────────────────────────────────────────────────────────────────────────
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '5000'}`;
const API_URL = process.env.API_URL ?? `ws://127.0.0.1:${process.env.PORT ?? '9000'}`;
/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {