* fix(proxy): reuse upstream connections for OpenCode API requests
`createProxyMiddleware` was constructed without an `agent`, so `http-proxy`
fell back to `agent: false`. That disables connection pooling and forces
`Connection: close` on every proxied request, consuming one ephemeral port
per request.
Measured against a real `opencode serve` instance, 200 sequential requests
through the proxy created 201 TIME_WAIT entries (1.005 ports/request). With
a keep-alive agent the same load creates 0.
On macOS the ephemeral range is 16,384 ports and TIME_WAIT lasts 30s, so
sustained traffic around 546 req/sec exhausts the pool — after which every
process on the host fails to open outbound connections with EADDRNOTAVAIL.
`maxSockets: Infinity` preserves the unbounded concurrency of `agent: false`,
so this changes connection reuse only, not request throughput.
Partially addresses #2915.
* fix(proxy): derive proxy agent class from the target scheme
Addresses review feedback on #2916. The first commit created an
unconditional `http.Agent`, which regresses external OpenCode servers
configured over https via `OPENCODE_HOST` (accepted by env-config.js).
http-proxy dispatches through `https.request` when the target protocol is
`https:` (http-proxy/lib/http-proxy/passes/web-incoming.js:126), and
`http.Agent#createConnection` is plain `net.createConnection` — so an
http.Agent would open a plaintext socket to a TLS port and fail every
proxied request. `agent: false` previously worked for both schemes.
`createOpenCodeProxyAgent(target)` now returns an `https.Agent` for https
targets and an `http.Agent` otherwise, derived once from
`resolveProxyTarget()` at registration so the single shared instance is
preserved across `apiProxy` and `interactiveOAuthProxy`.
Guarded in both test layers, verified to fail when the selection is
reverted to an unconditional http.Agent. `https.Agent` extends
`http.Agent`, so the http cases assert `not.toBeInstanceOf(https.Agent)`.
* Round 2: fix: resolve the proxy agent lazily so cold starts honor https
Addresses the round-2 blocker on #2916. Deriving the agent class at
registration is too early: startup-pipeline-runtime.js calls setupProxy()
(line 104) before bootstrapOpenCodeAtStartup() (line 141), so on a fresh
process state.openCodePort is null, buildOpenCodeUrl() throws
(network-runtime.js:86-88), and resolveProxyTarget() returns the http
loopback fallback. An external server configured via OPENCODE_HOST=https://
only appears on state.openCodeBaseUrl after bootstrap, so it was still
getting a plain http.Agent — the regression the previous commit intended
to fix.
`agent` is now a getter backed by a per-scheme memoizing resolver.
http-proxy-middleware rebuilds per-request options with
`Object.assign({}, this.proxyOptions)` in prepareProxyRequest, which invokes
getters, so resolution happens at request time while still yielding one
shared pool per scheme.
Tests now model the production ordering — registration while the port is
null and buildOpenCodeUrl throws, then an https base URL appearing after
bootstrap — and fail against the eager implementation. A behavioral test
pins the http-proxy-middleware option re-read the fix depends on, so a
library change that froze options would fail loudly instead of silently
regressing https targets.
The resolver is module-private; `bun run dead-code` flagged it as an
unused export when it was exported.
* Round 3: docs(changelog): note upstream connection reuse under [Unreleased]
Repo precedent adds [Unreleased] bullets for comparable proxy/stability
fixes (1.18.4 Stability, 1.9.3 Reliability/Proxy). Non-blocker raised in
review on #2916.
* Round 3: docs(changelog): use repo-standard 'behavior' spelling
* Round 4: docs(changelog): don't imply a restart is the only recovery
The ephemeral port pool drains on its own once the exhausting traffic
stops (TIME_WAIT expiry), so a restart is sufficient but not necessary.
Optional nit raised in review on #2916.
* Round 5: fix: construct the proxy agent through one factory; widen the pool
Review found the https branch was mutation-uncovered: the resolver
re-implemented agent construction inline instead of calling the exported
`createOpenCodeProxyAgent(target)`, so replacing its https branch with
`new https.Agent()` — dropping OPENCODE_AGENT_OPTIONS, and with it
keep-alive — left the entire suite green. Since `createOpenCodeProxyAgent`
also had no production callers, its four tests were pinning dead code.
Delegating collapses both: the factory is now the single construction
path, and the mutation fails 2 tests including the live resolver path.
Also from review:
- maxFreeSockets 32 -> 256 (Node's own default). The lower cap evicted
pooled sockets under concurrency, reintroducing the churn this agent
exists to prevent: at 64 concurrent requests it left 303 sockets in
TIME_WAIT versus 0 at 256.
- Added `timeout` to OPENCODE_AGENT_OPTIONS. Free-socket eviction is
governed by agent.options.timeout, which was unset, so idle sockets
persisted until the peer closed them. `keepAliveMsecs` is the TCP probe
delay, not the idle lifetime.
- resolveProxyTarget() now checks openCodePort before calling
buildOpenCodeUrl instead of relying on it throwing. The port is nulled
on several runtime paths (health-check failure, failed restart), so a
degraded OpenCode made every proxied request pay for a thrown-and-caught
exception — and the getter added a second call per request.
- Test fixtures use :4096 rather than :443; WHATWG URL elides the default
port, so parseInt('') is NaN and env-config rejects that host. The
fixtures modeled a state that cannot reach production.
- The getter-read assertion is now exact (0 at construction, 1, then 2)
rather than >= 2, which would have passed if the getter were read twice
at construction and never per-request.
- listen() rejects on 'error' and servers start inside try/finally, so a
bind failure fails the test instead of hanging to timeout.
openchamber # Start on port 3000
openchamber --port 8080# Custom port
openchamber --lan --port 3000# Listen on LAN (0.0.0.0)
openchamber --ui-password secret # Password-protect UI
openchamber startup enable# Start at login as a native serviceOPENCHAMBER_UI_PASSWORD=secret openchamber startup enable# Save service password env
openchamber startup status # Show startup service status
openchamber startup disable # Remove startup service
openchamber tunnel help# Tunnel lifecycle commands
openchamber tunnel providers # Show provider capabilities
openchamber tunnel profile add --provider cloudflare --mode managed-remote --name prod-main --hostname app.example.com --token <token>
openchamber tunnel start --profile prod-main
openchamber tunnel start --provider cloudflare --mode quick --qr
openchamber tunnel start --provider cloudflare --mode managed-local --config ~/.cloudflared/config.yml
openchamber tunnel status --all # Show tunnel state across instances
openchamber tunnel stop --port 3000# Stop tunnel only (server stays running)
openchamber connect-url --port 3000# Add this server to OpenChamber Desktop
openchamber connect-url --server http://host:3000 --qr
openchamber connect-url --port 3000 --qr
openchamber logs # Follow latest instance logsOPENCODE_PORT=4096OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode serverOPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber # Connect via custom host/HTTPS
openchamber stop # Stop server
openchamber update # Update to latest version
startup enable snapshots your current environment into the native service so startup behaves like you launched openchamber from the same shell. This preserves provider tokens, PATH, SSH agent settings, and other CLI auth/config env vars. Use --no-env-snapshot for a minimal service env.
When OpenChamber launches the local OpenCode server, it also registers a native
openchamber agent tool for project, session, and scheduled-task orchestration.
The tool is not injected when connecting to an external OpenCode server.
Behavior settings can optionally inject a managed system-prompt optimizer on
the next OpenCode restart. It is disabled by default and is not available for
external OpenCode servers.
Tunnel behavior notes
One active tunnel per running OpenChamber instance (port).
Starting a different tunnel mode/provider on the same instance replaces the active tunnel.
Replacing or stopping a tunnel revokes existing connect links and invalidates remote tunnel sessions.
Connect links are one-time tokens; generating a new link revokes the previous unused link.
Connect other OpenChamber apps
Use connect-url when a web/API server should be added to OpenChamber Desktop or another OpenChamber app. If no server is running on the selected port, OpenChamber starts one first.
--api-only starts API routes without serving browser UI assets. --lan binds the server so other machines can reach it. --server is the address saved into the Desktop connection link. --ui-password protects browser access if UI routes are enabled elsewhere; the generated client token is what Desktop uses for API access.
This creates a remote client token and prints an openchamber://connect?... link. The link contains the server URL, token, label, and payload version. In OpenChamber Desktop, paste it in Settings -> Remote Instances -> Direct Instances -> Import Link to add that server as an Instance.
If the server was started with --lan or --host 0.0.0.0, connect-url automatically advertises a detected LAN IP instead of 127.0.0.1. Use --server <url> when you want to advertise a specific DNS name, Tailscale address, reverse proxy URL, or HTTPS endpoint.
If you are exposing the server beyond localhost, start it with a password:
Generating a client token does not automatically password-protect the hosted browser UI. --ui-password protects browser access; the client token lets another OpenChamber app connect to this server.
Full base URL of external server (overrides OPENCODE_PORT)
OPENCODE_PORT
Port of external server
OPENCODE_SKIP_START
Skip starting embedded OpenCode server
OPENCHAMBER_OPENCODE_HOSTNAME
Bind hostname for managed OpenCode server (default: 127.0.0.1, use 0.0.0.0 for LAN/remote access — trusted networks only). Invalid values are rejected with an error and fall back to loopback
OPENCHAMBER_HOST
Bind hostname for the OpenChamber web server (default: 127.0.0.1; use 0.0.0.0 for LAN/remote access — trusted networks only)
OPENCHAMBER_VERBOSE_REQUEST_LOGS
Set to true to log every HTTP request; disabled by default to keep user logs small
OPENCHAMBER_SKIP_API_COMPRESSION
Set to true to disable gzip compression for /api/* responses
OPENCHAMBER_COMPRESS_API
Set to true to force /api/* compression, or false to disable it. Desktop runtime disables API compression by default to reduce local sidecar CPU use
OPENCHAMBER_TERMINAL_SHELL
Preferred terminal shell executable used by the Auto setting before platform defaults
Security note: binding to 0.0.0.0 exposes the server on all network interfaces — use only on trusted networks and protect with firewall rules or --ui-password.
Managed-local path note: OPENCHAMBER_TUNNEL_CONFIG must use a container path under /home/openchamber/.... If the config file references credentials-file, ensure that JSON path is also mounted and reachable inside the container.
Data directory: mount data/ for persistent storage. Ensure permissions:
openchamber # Runs in background by default
openchamber stop # Stop background server
systemd service (VPN / LAN access)
Use --foreground to keep the CLI process alive so systemd (or any other process manager) can track and restart it. Combine with OPENCODE_HOST to connect to an OpenCode instance running as a separate service.
Why set PATH and SSH_AUTH_SOCK?
systemd user services start with a minimal environment — no shell profile is sourced.
Without an explicit PATH, OpenCode won't find tools installed via Homebrew, npm, or ~/.local/bin.
Without SSH_AUTH_SOCK, git operations over SSH (push, pull, clone) will fail.
%t expands to $XDG_RUNTIME_DIR (e.g. /run/user/1000), where most SSH agents write their socket.
--host 0.0.0.0 is required to listen on all interfaces (the default is 127.0.0.1). Use --host <ip> or OPENCHAMBER_HOST=<ip> to bind to a specific interface instead.
What makes the web version special
Remote access - Cloudflare tunnel with QR onboarding. Scan from your phone, start coding.