import { execFileSync } from 'child_process' import { readFileSync } from 'fs' import { createRequire } from 'module' // look up a process's parent pid. returns null if we can't read it // (unsupported platform, process gone, permission denied). // // used by session-identity to walk one hop up from `process.ppid` — in a // Claude Code Bash tool the shell's parent is the agent-level process, // which is stable across invocations and distinct between agents. type Lookup = (pid: number) => number | null type CommLookup = (pid: number) => string | null let cached: Lookup | null = null let cachedComm: CommLookup | null = null export function getParentPid(pid: number): number | null { if (!cached) cached = build() try { return cached(pid) } catch { return null } } // short command name (comm) for a pid, or null when unreadable. used by the // owner-pid walk to stop at effectively-immortal session hosts (tmux/screen // servers and the Team Machine daemon), which must never become a sim owner. export function getProcessComm(pid: number): string | null { if (!cachedComm) cachedComm = buildComm() try { return cachedComm(pid) } catch { return null } } function buildComm(): CommLookup { if (process.platform === 'linux') { return (pid) => { try { return readFileSync(`/proc/${pid}/comm`, 'utf8').trim() || null } catch { return null } } } if (process.platform === 'darwin') { const ffi = tryBuildDarwinCommFfi() if (ffi) return ffi return (pid) => readProcessTable().get(pid)?.comm || null } return () => null } function build(): Lookup { // on any platform, if the shell exports $PPID and it differs from the // node-level process.ppid, it's the shell's own parent — one level up // from where we need to be. this is the cheapest and most portable // grand-ppid source since it needs no subprocess or FFI. const envPpid = Number(process.env.PPID) if (Number.isFinite(envPpid) && envPpid > 1 && envPpid !== process.ppid) { return () => envPpid } if (process.platform === 'linux') return buildLinux() if (process.platform === 'darwin') { // on Bun: libproc via FFI is ~0.8µs/call. on Node: one `ps` snapshot of // the whole tree (see readProcessTable). const ffi = tryBuildDarwinFfi() if (ffi) return ffi return (pid) => { const ppid = readProcessTable().get(pid)?.ppid return ppid !== undefined && ppid > 0 ? ppid : null } } return () => null } // one snapshot of the whole process tree, shared by the ppid and comm lookups. // macOS has no /proc and Node cannot call libproc, so each lookup used to shell // out to `lsof -R -p ` with a 2000ms budget — measured at 3.5s, 7.1s and // 7.9s on a loaded box, so every hop of an ancestor walk timed out, the walk // resolved to null, and `contrast ide open` launched a browser host with an // empty owner pid that `ide close` could never attribute. `ps -eo pid,ppid,comm` // answers for every process at once in ~0.1-0.9s. the short TTL keeps one walk // on one snapshot while a long-lived caller still sees the tree change. const PROCESS_TABLE_TTL_MS = 2000 let processTable: { readAt: number; rows: Map } = { readAt: 0, rows: new Map() } function readProcessTable(): Map { const now = Date.now() if (processTable.readAt && now - processTable.readAt < PROCESS_TABLE_TTL_MS) { return processTable.rows } const rows = new Map() try { const out = execFileSync('ps', ['-eo', 'pid=,ppid=,comm='], { encoding: 'utf8', timeout: 5000, maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'], }) for (const line of out.split('\n')) { const parsed = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/) if (!parsed) continue // libproc reports pbi_comm — the executable's basename, truncated to // MAXCOMLEN — and the immortal-host patterns are written against that // shape, while `ps -o comm` prints the full path. normalize to one. rows.set(Number(parsed[1]), { ppid: Number(parsed[2]), comm: (parsed[3].split('/').at(-1) ?? '').slice(0, PBI_COMM_LEN), }) } } catch { // an unreadable table means unresolved ancestry, which callers already // treat as "no defensible owner" and refuse on. } processTable = { readAt: now, rows } return rows } function buildLinux(): Lookup { return (pid) => { try { const raw = readFileSync(`/proc/${pid}/stat`, 'utf8') const end = raw.lastIndexOf(')') if (end < 0) return null const rest = raw .slice(end + 1) .trim() .split(/\s+/) const ppid = Number(rest[1]) return Number.isFinite(ppid) && ppid > 0 ? ppid : null } catch { return null } } } // PROC_PIDTBSDINFO flavor for proc_pidinfo. struct proc_bsdinfo is 216 bytes; // pbi_ppid is at offset 16 (after pbi_flags, pbi_status, pbi_xstatus, pbi_pid, // each 4 bytes). source: in the macOS SDK. const PROC_PIDTBSDINFO = 3 const PROC_BSDINFO_SIZE = 216 const PBI_PPID_OFFSET = 16 // char pbi_comm[MAXCOMLEN] — after the 4-byte flags/status/xstatus/pid/ppid/ // uid/gid/ruid/rgid/svuid/svgid/rfu_1 fields. source: . const PBI_COMM_OFFSET = 48 const PBI_COMM_LEN = 16 function tryBuildDarwinFfi(): Lookup | null { if (!process.versions?.bun) return null try { const r = createRequire(import.meta.url) const { dlopen, FFIType } = r('bun:ffi') const lib = dlopen('/usr/lib/libproc.dylib', { proc_pidinfo: { args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32], returns: FFIType.i32, }, }) const buf = Buffer.alloc(PROC_BSDINFO_SIZE) return (pid) => { const ret = Number( lib.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0n, buf, PROC_BSDINFO_SIZE), ) if (ret <= 0) return null const ppid = buf.readUInt32LE(PBI_PPID_OFFSET) return ppid > 0 ? ppid : null } } catch { return null } } function tryBuildDarwinCommFfi(): CommLookup | null { if (!process.versions?.bun) return null try { const r = createRequire(import.meta.url) const { dlopen, FFIType } = r('bun:ffi') const lib = dlopen('/usr/lib/libproc.dylib', { proc_pidinfo: { args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32], returns: FFIType.i32, }, }) const buf = Buffer.alloc(PROC_BSDINFO_SIZE) return (pid) => { const ret = Number( lib.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0n, buf, PROC_BSDINFO_SIZE), ) if (ret <= 0) return null const raw = buf.subarray(PBI_COMM_OFFSET, PBI_COMM_OFFSET + PBI_COMM_LEN) const end = raw.indexOf(0) const comm = raw.subarray(0, end < 0 ? raw.length : end).toString('utf8') return comm || null } } catch { return null } }