fix(server): fix daemon startup race and reduce log bloat

The desktop app could time out connecting to the daemon on first launch
because the PID file advertised a listen address before the HTTP server
was actually listening. The supervisor now writes the PID lock with
listen: null and updates it only after the worker sends a paseo:ready
IPC message confirming the server is listening.

- Rename daemon-runner.ts to supervisor-entrypoint.ts
- PID lock acquired with listen: null, updated via paseo:ready IPC
- Worker sends paseo:ready after httpServer.listen() resolves
- Desktop polls until listen is non-null before returning to the app
- Remove PASEO_PID_LOCK_MODE and external lock mode
- Remove unnecessary env overrides (PASEO_HOME, PASEO_CORS_ORIGINS) from
  desktop daemon spawn
- Reduce trace log bloat: inbound/outbound WebSocket messages now log
  only message type and payload size instead of full payloads
- Supervisor restarts worker on SIGKILL (covers OOM)
This commit is contained in:
Mohamed Boudra
2026-03-30 12:37:07 +07:00
parent e758734807
commit 8d0a9f9224
21 changed files with 198 additions and 147 deletions
@@ -124,11 +124,16 @@ function resolveDaemonRunnerEntry(): string {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string };
if (packageJson.name === "@getpaseo/server") {
const distRunner = path.join(currentDir, "dist", "scripts", "daemon-runner.js");
const distRunner = path.join(
currentDir,
"dist",
"scripts",
"supervisor-entrypoint.js",
);
if (existsSync(distRunner)) {
return distRunner;
}
return path.join(currentDir, "scripts", "daemon-runner.ts");
return path.join(currentDir, "scripts", "supervisor-entrypoint.ts");
}
} catch {
// Continue searching up if package.json exists but is invalid.
@@ -2,7 +2,7 @@
/**
* Regression: `paseo daemon stop` must stop supervised dev daemons
* without allowing daemon-runner to respawn a new worker process.
* without allowing the supervisor entrypoint to respawn a new worker process.
*/
import assert from "node:assert";
@@ -120,20 +120,24 @@ let supervisorProcess: ChildProcess | null = null;
let recentSupervisorLogs = "";
try {
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `127.0.0.1:${port}`,
PASEO_RELAY_ENABLED: "false",
CI: "true",
supervisorProcess = spawn(
"npx",
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
{
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `127.0.0.1:${port}`,
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
stdio: ["ignore", "pipe", "pipe"],
},
stdio: ["ignore", "pipe", "pipe"],
});
);
supervisorProcess.stdout?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
@@ -161,8 +165,9 @@ try {
const command = readProcessCommand(daemonPid);
assert(command !== null, "pid lock pid should resolve to a running process command");
assert(
command.includes("daemon-runner.ts") || command.includes("daemon-runner.js"),
`pid lock pid should be daemon-runner process, got: ${command}`,
command.includes("supervisor-entrypoint.ts") ||
command.includes("supervisor-entrypoint.js"),
`pid lock pid should be supervisor-entrypoint process, got: ${command}`,
);
console.log(`✓ dev daemon started with daemon pid ${daemonPid}\n`);
@@ -186,7 +191,7 @@ try {
await waitFor(
() => !isProcessRunning(supervisorProcess!.pid ?? -1),
15000,
"daemon-runner supervisor remained running after stop",
"supervisor-entrypoint process remained running after stop",
);
}
@@ -1,7 +1,7 @@
#!/usr/bin/env npx tsx
/**
* Regression: a single SIGINT sent to a supervised daemon-runner must allow
* Regression: a single SIGINT sent to the supervised supervisor entrypoint must allow
* graceful daemon lifecycle shutdown to complete (no early forced exit path).
*/
@@ -131,21 +131,25 @@ let supervisorProcess: ChildProcess | null = null;
let recentSupervisorLogs = "";
try {
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `127.0.0.1:${port}`,
PASEO_RELAY_ENABLED: "false",
CI: "true",
supervisorProcess = spawn(
"npx",
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
{
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: `127.0.0.1:${port}`,
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
},
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
});
);
supervisorProcess.stdout?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
@@ -62,7 +62,7 @@ try {
"-e",
// Keep the process alive long enough for stop command assertions.
"setInterval(() => {}, 1000)",
"daemon-runner.ts",
"supervisor-entrypoint.ts",
],
{
env: {
@@ -122,20 +122,24 @@ let supervisorProcess: ChildProcess | null = null;
let recentSupervisorLogs = "";
try {
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: host,
PASEO_RELAY_ENABLED: "false",
CI: "true",
supervisorProcess = spawn(
"npx",
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
{
cwd: cliRoot,
env: {
...process.env,
...testEnv,
PASEO_HOME: paseoHome,
PASEO_LISTEN: host,
PASEO_RELAY_ENABLED: "false",
CI: "true",
},
stdio: ["ignore", "pipe", "pipe"],
},
stdio: ["ignore", "pipe", "pipe"],
});
);
supervisorProcess.stdout?.on("data", (chunk) => {
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
@@ -134,11 +134,6 @@ try {
env: {
...process.env,
...testEnv,
// This test validates direct unsupervised worker ownership semantics.
// Agent-orchestrated shells may export PASEO_PID_LOCK_MODE=external,
// which would delegate lock ownership away from this process and make
// daemon status checks fail to observe a running owner PID.
PASEO_PID_LOCK_MODE: "self",
PASEO_HOME: paseoHome,
PASEO_LISTEN: host,
PASEO_RELAY_ENABLED: "false",