(evolverRoot)
| 203 | // - Execution errors are swallowed: this must never cause session-start to |
| 204 | // error out or delay the LLM context injection. |
| 205 | function _maybeRestartDaemon(evolverRoot) { |
| 206 | try { |
| 207 | var autoRestart = String(process.env.EVOLVER_SESSION_AUTO_RESTART || '1').toLowerCase().trim(); |
| 208 | if (autoRestart === '0' || autoRestart === 'false') return; |
| 209 | |
| 210 | var lifecyclePath = evolverRoot |
| 211 | ? path.join(evolverRoot, 'src', 'ops', 'lifecycle.js') |
| 212 | : null; |
| 213 | if (!lifecyclePath || !fs.existsSync(lifecyclePath)) return; |
| 214 | |
| 215 | // Check if daemon is running by looking for the PID file / lock file. |
| 216 | // Lock resolution + lease staleness live in ./_lockPaths (issue #176) — |
| 217 | // the same module index.js uses, so the hook can never drift from the |
| 218 | // daemon again. It is fs/os/path-only, keeping index.js out of the |
| 219 | // hook's require graph (R12), and ships in hookAdapter.js's copy list |
| 220 | // for the deployed `.claude/hooks/` layout (PR #163). |
| 221 | var lockFile = lockPaths.getLockFilePath(); |
| 222 | var daemonRunning = false; |
| 223 | try { |
| 224 | if (fs.existsSync(lockFile)) { |
| 225 | var raw = fs.readFileSync(lockFile, 'utf8').trim(); |
| 226 | var payload = raw && raw[0] === '{' ? JSON.parse(raw) : { pid: parseInt(raw, 10) }; |
| 227 | if (payload && payload.pid > 0) { |
| 228 | // R1: PID-reuse defense. process.kill(pid, 0) only proves SOME |
| 229 | // process owns that PID -- after macOS sleep / OOM, the kernel may |
| 230 | // have reused the slain daemon's PID for an unrelated process. |
| 231 | try { process.kill(payload.pid, 0); daemonRunning = true; } catch (e) { |
| 232 | // EPERM = process exists but owned by a different user; still a live daemon. |
| 233 | if (e && e.code === 'EPERM') daemonRunning = true; |
| 234 | } |
| 235 | // Lease staleness overrides kill(0)=alive: a lease-aware daemon |
| 236 | // refreshes the lock mtime on a timer, so an expired lease means |
| 237 | // dead/wedged regardless of kill(0). Pre-lease locks are never |
| 238 | // judged stale by mtime (lockIsStaleByLease handles both). |
| 239 | if (daemonRunning && lockPaths.lockIsStaleByLease(lockFile, payload)) { |
| 240 | daemonRunning = false; |
| 241 | } |
| 242 | } |
| 243 | } |
| 244 | } catch (_) { /* lock file unreadable or absent: assume not running */ } |
| 245 | |
| 246 | if (daemonRunning && _proxyHealthyIfExpected()) return; // already alive, nothing to do |
| 247 | |
| 248 | // Daemon appears dead. Spawn lifecycle.js start in the background so |
| 249 | // this session-start script exits immediately (< 50 ms) and does not |
| 250 | // block the LLM from getting context. |
| 251 | var { spawn } = require('child_process'); |
| 252 | var child = spawn( |
| 253 | process.execPath, |
| 254 | [lifecyclePath, 'start'], |
| 255 | { |
| 256 | detached: true, |
| 257 | stdio: 'ignore', |
| 258 | cwd: evolverRoot, |
| 259 | env: Object.assign({}, process.env), |
| 260 | windowsHide: true, |
| 261 | } |
| 262 | ); |
no test coverage detected