Skip to content

Resource Allocation Catalog: Dashboard Operation

Date: 2026-02-24 Task: d8d4d862 — Identify and catalog all port bindings and resource allocations Scope: packages/web/, web.js

This document is a focused reference for all network ports, file handles, and system resources allocated during dashboard operation. For process-lifecycle and signal-handling details see process-lifecycle-audit.md and signal-handling-audit.md.


1. Port Bindings

1.1 HTTP / WebSocket / MCP Port (Single Binding)

PropertyValue
Default port3117
Fallback range3117–3200 (sequential scan)
ProtocolTCP
Bound bystart.ts server.listen(actualPort)
Shared overHTTP requests, WebSocket upgrades (Upgrade header), MCP Streamable HTTP (/mcp/*)
Sourcepackages/web/src/server/port.tsDEFAULT_PORT, PORT_RANGE_START, PORT_RANGE_END

Only one TCP port is bound per server run. All three protocols (HTTP, WebSocket, MCP) share that single port via in-process routing:

  • HTTP routes are matched by URL path prefix (/api/*, /data/*, /mcp/*, /).
  • WebSocket upgrades are detected via the upgrade event on http.Server.
  • MCP sessions are routed by URL path (/mcp/rex, /mcp/sourcevision).

Port allocation algorithm (packages/web/src/server/port.ts):

  1. Try preferred port (from --port flag → .n-dx.json → env PORT3117) with up to 5 retries, exponential back-off starting at 100 ms × 2 multiplier.
  2. EACCES (permission denied) → fail immediately; no fallback.
  3. After retry exhaustion → scan 3117–3200 sequentially for the first free port.
  4. Server binds and writes the actual port to .n-dx-web.port.

No other TCP or UDP ports are bound.


2. File Handles

2.1 Written Files (Server Creates / Owns)

FileCreated ByContentsRemoved By
.n-dx-web.portServer process (start.ts line 502)Actual bound port number (plain text)Step 4 of graceful shutdown; exit safety handler
.n-dx-web.pidOrchestrator (web.js) after reading port file{"pid":N,"port":N,"startedAt":"…"}web.js SIGINT/SIGTERM handler; ndx start stop

Both files live in the project root directory (the path passed to ndx start).

2.2 Read-Only Files Opened at Runtime

FileReaderWhen
.n-dx.jsonweb.js, start.tsOnce at startup; not re-read
.hench/config.jsonroutes-hench.tsOn each relevant API request
.hench/runs/*.jsonroutes-hench.tsOn status/run-list API requests
.rex/prd.jsonroutes-rex.tsOn each relevant API request
.rex/config.jsonroutes-rex.tsOn startup and config API requests
.sourcevision/CONTEXT.mdroutes-sourcevision.tsOn sv_context API requests
.sourcevision/manifest.jsonroutes-sourcevision.tsOn sv_inventory and related requests

These files are opened, read, and closed on demand. No persistent file descriptors are held between requests.

2.3 File Watchers (fs.watch)

fs.watch() registers kernel-level directory watchers. Each occupies a file descriptor in the server process.

WatcherDirectory WatchedTrigger ConditionWebSocket EventCreated In
Sourcevision watcher.sourcevision/File in ALL_DATA_FILES listsv:data-changedstart.ts registerSourcevisionWatcher()
Rex watcher.rex/prd.json changesrex:prd-changedstart.ts registerRexWatcher()
Hench watcher.hench/runs/Any *.json filehench:run-changedstart.ts registerHenchWatcher()
Dev viewer watcherdirname(viewerPath)index.html changesviewer:reloadstart.ts registerDevViewerWatcher() (dev mode only)

Watchers are not explicitly .close()d during shutdown. They are implicitly released when the process exits. The fs.watch call is wrapped in try/catch because it is not guaranteed on all filesystems.


3. In-Process Resources

3.1 Timers

TimerIntervalPurposeCreated ByCleanup
WebSocket ping keepalive30 sDetect dead clients (no-pong → destroy socket)websocket.ts — on first client connectclearInterval() in ws.shutdown()explicit, no leak
Heartbeat monitor30 sDetect unresponsive hench runs; broadcast hench:heartbeat-alertroutes-hench.ts startHeartbeatMonitor().unref() called at line 1201 — won't block exit, but timer is never explicitly cleared
Task state transitionOne-shot 1 sTransition hench task from "starting" to "running"routes-hench.ts handleExecute()Self-cancels after firing

3.2 Child Processes

ResourceCommandCountSpawned ByCleanup
Hench task runnerhench run --task=<id>0–N concurrentroutes-hench.ts handleExecute() via spawnManagedshutdownActiveExecutions()killWithFallback(5 s SIGTERM→SIGKILL)
Rex epic-by-epic runnerhench run --epic=<id> --loop --auto0–1 (singleton)routes-rex.ts runHenchForEpic() via spawnManagedshutdownRexExecution()killWithFallback(5 s SIGTERM→SIGKILL)
Background server processnode packages/web/dist/cli/index.js serve0–1 (background mode only)web.js via spawn --detachedndx start stop sends SIGTERM to PID in .n-dx-web.pid

3.3 In-Memory State Maps

ObjectLocationContentsLifetime
activeExecutionsroutes-hench.ts module scopeMap<taskId, {runId, handle, state}>Entries added on task start, removed on completion
henchProcessroutes-rex.ts module scopeManagedChild | null — rex epic-runner singletonSet during run, nulled on exit
rexSessionsroutes-mcp.ts module scopeMap<sessionId, {transport, server}>Entry per active MCP client; removed on transport close
svSessionsroutes-mcp.ts module scopeMap<sessionId, {transport, server}>Entry per active MCP client; removed on transport close
alertedRunsroutes-hench.ts startHeartbeatMonitor() closureMap<runId, HeartbeatStatus>Lives for server lifetime (no eviction)

3.4 HTTP Server

PropertyDetail
Typehttp.Server (Node.js built-in)
Createdstart.ts createServer(requestHandler)
HoldsOne TCP socket per in-flight HTTP connection
Cleanupserver.close(callback) in graceful shutdown step 3 — stops accepting; drains active requests

3.5 WebSocket Connections

PropertyDetail
TypeRaw RFC 6455 framing over http.Server upgrade event
Createdstart.ts createWebSocketManager()
StateSet<{socket: Socket, alive: boolean}> — one entry per connected browser tab
Cleanupws.shutdown() in graceful shutdown step 2: clearInterval(ping), close frame to each client, socket.destroy()

4. Resource Cleanup Procedures

4.1 Four-Step Graceful Shutdown (start.ts)

Triggered by SIGINT or SIGTERM. A N_DX_SHUTDOWN_TIMEOUT_MS-second (default 30 s) watchdog fires process.exit(1) if any step stalls.

Step 1 — Child processes (parallel, up to 5 s each)
  shutdownActiveExecutions()   → SIGTERM each hench task child → SIGKILL after 5 s
  shutdownRexExecution()       → SIGTERM rex epic-runner     → SIGKILL after 5 s

Step 2 — WebSocket connections
  ws.shutdown()  → clearInterval(ping) → RFC 6455 close frame → socket.destroy()

Step 3 — HTTP server
  server.close(callback)   → stop accepting; drain in-flight requests

Step 4 — Port file + exit
  unlink(.n-dx-web.port) → process.exit(0)

Second SIGINT/SIGTERM during shutdown → process.exit(1) immediately (escape hatch).

4.2 killWithFallback Protocol (packages/llm-client/src/exec.ts)

Applied to every child process in steps 1:

  1. child.kill("SIGTERM") — request cooperative shutdown.
  2. Wait up to HENCH_SHUTDOWN_TIMEOUT_MS (default 5 000 ms) for close event.
  3. If still alive → child.kill("SIGKILL") — force terminate.
  4. Wait up to 1 000 ms for close, then continue regardless.

4.3 Orchestrator Cleanup (web.js)

web.js installs SIGINT/SIGTERM handlers that remove .n-dx-web.pid and .n-dx-web.port. This is the orchestrator cleanup path (foreground mode only). The detached server process runs its own graceful shutdown independently.

4.4 Forced Stop (ndx start stop)

Reads PID from .n-dx-web.pid, sends SIGTERM, waits N_DX_STOP_GRACE_MS (default 2 000 ms), sends SIGKILL if still running.


5. Resource Summary Table

ResourceCountLifespanCleanupNotes
TCP port (HTTP/WS/MCP)1Full server runserver.close() step 3Shared by all protocols
.n-dx-web.port file1Server runRemoved step 4 + exit handlerSignals readiness to orchestrator
.n-dx-web.pid file1Orchestrator runweb.js SIGINT/SIGTERMBackground mode only
http.Server1Full server runserver.close() step 3
WebSocket manager1Full server runws.shutdown() step 2
WebSocket clients0–NPer-connectionClose frame + destroy step 2
WS ping interval1First client → shutdownclearInterval step 2Explicit, no leak
Heartbeat monitor timer1Full server run.unref() only — implicit exitDoesn't block exit
fs.watch (sv dir)0–1Full server runImplicit on exitConditional on scope
fs.watch (rex dir)0–1Full server runImplicit on exitConditional on scope
fs.watch (hench dir)0–1Full server runImplicit on exitConditional on scope
fs.watch (viewer dir)0–1Full server runImplicit on exitDev mode only
Hench task children0–NPer taskkillWithFallback step 1
Rex epic-runner child0–1Per epic runkillWithFallback step 1
MCP session objects0–MPer MCP clientImplicit on HTTP server closeNo OS resources
activeExecutions map1Full server runEntries removed per task
rexSessions / svSessions maps2Full server runEntries removed per MCP session

6. Environment Variable Controls

VariableDefaultScopeEffect
N_DX_SHUTDOWN_TIMEOUT_MS30000start.tsHard deadline for full shutdown; process.exit(1) on breach
HENCH_SHUTDOWN_TIMEOUT_MS5000routes-hench.ts, routes-rex.tsSIGTERM→SIGKILL grace period per child
N_DX_STOP_GRACE_MS2000web.jsGrace period in ndx start stop before SIGKILL
PORTweb.jsAlternative to --port flag for port selection

7. Known Gaps

GapLocationDescriptionSeverity
Heartbeat monitor not explicitly clearedroutes-hench.ts line 1201startHeartbeatMonitor() timer has no exported cleanup function; relies on .unref() + process exitLow — .unref() prevents blocking exit
fs.watch handles not explicitly closedstart.ts watcher registrationWatcher objects are not stored and closed during shutdownLow — released implicitly on exit
web.js doesn't signal background child on SIGINT/SIGTERMweb.js lines 442–448Orchestrator removes PID/port files but doesn't send signal to the detached server processLow — ndx start stop is the intended stop path
Hench run.ts handles SIGINT but not SIGTERMpackages/hench/src/cli/run.tsTwo-stage graceful shutdown only activates on SIGINT; SIGTERM uses default (immediate) terminationMedium for observability; low for correctness

Released under the Elastic License 2.0.