diff --git a/browse/src/error-handling.ts b/browse/src/error-handling.ts index 3be921fa2..90bfb17f2 100644 --- a/browse/src/error-handling.ts +++ b/browse/src/error-handling.ts @@ -36,7 +36,15 @@ export function safeKill(pid: number, signal: NodeJS.Signals | number): void { } } -/** Check if a PID is alive. Pure boolean probe — returns false for ALL errors. */ +/** + * Check if a PID is alive. Pure boolean probe — never throws. + * + * EPERM means the process EXISTS but we lack rights to signal it. That is + * alive — returning false there makes callers that validate liveness before + * killing (killAgentByRecord, the terminal-agent watchdog) skip the kill and + * respawn around a survivor, leaking one process per watchdog tick (#2414, + * #2295). + */ export function isProcessAlive(pid: number): boolean { if (IS_WINDOWS) { try { @@ -52,7 +60,7 @@ export function isProcessAlive(pid: number): boolean { try { process.kill(pid, 0); return true; - } catch { - return false; + } catch (err: any) { + return err?.code === 'EPERM'; } } diff --git a/test/egress-lib.test.ts b/test/egress-lib.test.ts index 2cfb51c45..faa0f63b7 100644 --- a/test/egress-lib.test.ts +++ b/test/egress-lib.test.ts @@ -17,7 +17,7 @@ import * as os from 'os'; import * as path from 'path'; import { listReceipts, sha256Hex } from '../lib/egress-receipt'; -const ROOT = path.resolve(new URL(import.meta.url).pathname, '..', '..'); +const ROOT = path.resolve(import.meta.path, '..', '..'); const LIB = path.join(ROOT, 'bin', 'gstack-egress-lib.sh'); const received: string[] = []; diff --git a/test/egress-receipt-wiring.test.ts b/test/egress-receipt-wiring.test.ts index 484a1e1ff..0ac0ae67e 100644 --- a/test/egress-receipt-wiring.test.ts +++ b/test/egress-receipt-wiring.test.ts @@ -28,7 +28,7 @@ import { describe, test, expect } from 'bun:test'; import * as fs from 'fs'; import * as path from 'path'; -const ROOT = path.resolve(new URL(import.meta.url).pathname, '..', '..'); +const ROOT = path.resolve(import.meta.path, '..', '..'); function read(rel: string): string { return fs.readFileSync(path.join(ROOT, rel), 'utf-8'); diff --git a/test/egress-receipt.test.ts b/test/egress-receipt.test.ts index 64e6f3928..233d7695d 100644 --- a/test/egress-receipt.test.ts +++ b/test/egress-receipt.test.ts @@ -31,7 +31,7 @@ import { writeReceipt, } from '../lib/egress-receipt'; -const ROOT = path.resolve(new URL(import.meta.url).pathname, '..', '..'); +const ROOT = path.resolve(import.meta.path, '..', '..'); let home: string; diff --git a/test/gstack-egress-cli.test.ts b/test/gstack-egress-cli.test.ts index 4881de80f..6d9f19f8d 100644 --- a/test/gstack-egress-cli.test.ts +++ b/test/gstack-egress-cli.test.ts @@ -19,7 +19,7 @@ import { writeReceipt, } from '../lib/egress-receipt'; -const ROOT = path.resolve(new URL(import.meta.url).pathname, '..', '..'); +const ROOT = path.resolve(import.meta.path, '..', '..'); const BIN = path.join(ROOT, 'bin', 'gstack-egress'); let home: string; diff --git a/test/pty-skill-seeding-wiring.test.ts b/test/pty-skill-seeding-wiring.test.ts index 01f5999ce..bd715a9b4 100644 --- a/test/pty-skill-seeding-wiring.test.ts +++ b/test/pty-skill-seeding-wiring.test.ts @@ -17,7 +17,7 @@ import { describe, test, expect } from 'bun:test'; import * as fs from 'fs'; import * as path from 'path'; -const ROOT = path.resolve(new URL(import.meta.url).pathname, '..', '..'); +const ROOT = path.resolve(import.meta.path, '..', '..'); /** A PTY send whose payload starts with a slash command (`/name`, optionally * followed by `\r`, whitespace, or the closing quote). A second slash right diff --git a/test/skill-census.test.ts b/test/skill-census.test.ts index b9bb21578..f2ea7e5c8 100644 --- a/test/skill-census.test.ts +++ b/test/skill-census.test.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { frontmatterName, skillCensus } from './helpers/skill-census'; -const ROOT = path.resolve(new URL(import.meta.url).pathname, '..', '..'); +const ROOT = path.resolve(import.meta.path, '..', '..'); const census = skillCensus(ROOT); describe('skillCensus', () => {