PM2 processes with a deleted cwd lie about their own fleet
Symptom
/api/swarm/health on Nexus (Zappo) reports observability_halt with
PM2 unhealthy: nexus-q status=unknown; PM2 unhealthy: nexus-omega status=unknown; PM2 unhealthy: nexus-aether status=unknown; PM2 unhealthy: nexus-x status=unknown
while PM2 itself shows all four engines online with live PIDs and heartbeats
seconds old.
Cause
nexus-api (PID 1284, started 2026-09-15 09:27 AEST) and nexus-health
(PID 1322, same start) were launched from /Volumes/FATTY/src/nexus. The
/Volumes/FATTY volume was remounted at 2026-09-15 11:20, which orphaned those
processes' working-directory inode. lsof -p <pid> -d cwd returns no row for
both PIDs, which is the tell.
Node's process.cwd() then throws ENOENT: no such file or directory, uv_cwd.
_pm2_status_map() in /Volumes/FATTY/src/nexus/api.py shells out to
pm2 jlist; the pm2 CLI loads cwd at bootstrap (pm2/lib/API.js:69) and exits 1
before reading the daemon. The API therefore reports status=unknown for every
engine and raises an observability halt.
Reproduced 2026-09-17 on Zappo: chdir into a directory, rmdir it, then run
pm2 jlist → exit 1 with the identical uv_cwd traceback. Running pm2 jlist
from an intact cwd always exits 0, so this is not a PM2 daemon or PATH problem.
Evidence: 806 Unable to read PM2 status lines in
~/.pm2/logs/nexus-api-error.log; 12,424 PM2 unhealthy lines in
~/.pm2/logs/nexus-health-error.log.
Rule
A PM2-managed process can stay online and serve HTTP while its cwd is gone.
Health probes that shell out to pm2 from inside such a process fail silently
and report the whole fleet as unknown. When PM2 status disagrees with heartbeats:
- Compare
lsof -a -p <pid> -d cwd -Fnagainstpm2 jlist'spm_cwd. An empty lsof cwd row is the false-positive signature. - Trust the real PID plus the engine heartbeat (
/api/healthengines.*), not the derivedpm2_statusfield. - Clear it with
pm2 restart <app>so PM2 re-spawns the process in the current mount. Do not restart the engines, which are healthy. restart_time=0plus anonlinePID does not clear the fault. Bothnexus-api(pid 1284) andnexus-health(pid 1322) have shown 0 restarts and no cwd row at the same time on 2026-09-18, because they started before the 2026-09-15 11:20 remount and never respawned. Onlylsof -a -p <pid> -d cwdsettles it.- Fix the caller, not just the process.
api.pycallssubprocess.check_output(["pm2", "jlist"], timeout=6)with nocwd=, so it inherits the dead directory and dies. Passingcwd=BASE_DIRmakes the same call succeed from a deleted cwd (verified 2026-09-18: inherited cwd -> CalledProcessError exit 1;cwd="/Volumes/FATTY/src/nexus"-> exit 0, 33 apps parsed).health_check.pyalready does this in_run()(cwd=str(BASE_DIR), line 122), and its error log has 0uv_cwdlines as a result. Onlynexus-apineeds the restart;nexus-healthreads PM2 fine and itsPM2 unhealthylines are about genuinely stopped idle apps (nexus-metrics, nexus-printer, nexus-profit-tracker), not the engines. - Scope the blast radius before calling it fail-closed. The observability provider
is registered only inside
api.py'sget_swarm_engine()(line 148), so theis_trading_allowed()gate only holds that state in the API process. The four engine processes never register a provider (set_observability_providerappears nowhere else outside tests), so their_observability_haltstays False and the gate returns early. Engine error logs carry noHEALTH_HALTorOBSERVABILITY HALTlines. Earlier notes calling this halt "fail-closed on new entries" were wrong about the live engines;/api/swarmreportingtrading_allowed: falseis the API's own view.
Recurrence log
-
2026-09-18 18:05 AEST: still engaged,
nexus-apipid 1284 unchanged since 2026-09-15 09:27 (0 restarts, emptylsof -a -p 1284 -d cwd). The four engines were restarted 2026-09-17 20:23-20:35, so theirpm_cwdis current and PM2 shows themonlinewith live PIDs; only the API is stale.pm2 jlistfrom a removed cwd still exits 1 with theuv_cwdtraceback. Fix is unchanged:pm2 restart nexus-api(API only, engines are healthy and untouched). 1,888returned non-zero exit status 1lines in the API error log by 18:06. -
2026-09-18 21:05 AEST: unchanged at the next monitor run. The scheduled
pro_nexus_finance_monitor_report.pyreportedswarm degraded: OBSERVABILITY HALTwithnexus-q/omega/aether/x status=unknown, checked 2026-09-18 21:05:44. Independently confirmed: PM2 shows all four enginesonlinewith live PIDs (nexus-q 62949, nexus-x 67643, nexus-omega 60393, nexus-aether 61251, one restart each, started 2026-09-17 20:23-20:35) and heartbeats seconds old (omega 21:08:09, x 21:07:41, aether 21:07:07, q 21:02:25).nexus-apipid 1284 still unfixed (0 restarts, started 2026-09-15 09:27). Counters: 2,043returned non-zero exit status 1lines and 5,092uv_cwdlines innexus-api-error.log;nexus-health-error.loghas 0uv_cwdlines and 13,273PM2 unhealthylines, all idle stopped apps.halt_tradingstayedfalse.pm2 restart nexus-apiremains the whole fix and had not been run because it is a production change awaiting owner approval.
Related
halt_tradingstayedfalsethroughout, so this was an observability fault, not a trading fault. Checkhalt_tradingbefore escalating severity.