Files
orca/config/scripts/daemon-replacement-process-inspection.mjs
T
Neil 063827d4b1 fix(repro): stop the script doing the exact thing it exists to warn about
Both findings are right, and the first is pointed: this script demonstrates that
signalling a pid you have not re-verified can kill someone else's work, and its own
teardown did that twice.

The staged daemon is killed on purpose in phase 1. Once Node reaps that child its
pid is free for reuse, and teardown signalled the remembered number anyway; it now
signals only while the child object still reports no exit.

The markers were half-verified. Each proves its own identity by tag before being
signalled, but its session leader was killed on a number remembered from staging a
minute earlier — and the leader is the one pid here that can be recycled while its
child lives on under a new parent. The leader is now re-read from the live marker
and signalled only when the marker still claims it.

Second finding: isRealUserDaemon matched a hardcoded macOS userData path, so on
Linux a real daemon could never be recognised — the assertion that this run harmed
nothing was inert on the platform where nobody would notice. Now matched per
platform, with a loose fallback rather than a silent false.

Repro re-run end to end: all three phases pass, pre-existing daemons still running,
real userData daemon untouched.
2026-08-12 18:14:08 -07:00

137 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Process-table helpers for the daemon PTY preservation repro: what is alive, what a pid is
* running, and which daemons were already here before the run. Split out so the repro script
* itself stays about the sequence it proves rather than the plumbing it proves it with.
*/
import { execFileSync } from 'node:child_process'
// Electron's userData path differs per platform, and hardcoding the macOS one meant a real
// daemon could never be recognised on Linux — so the guard that this run harmed nothing was
// inert on exactly the platform where it would go unnoticed.
const REAL_USER_DAEMON_MARKERS = {
darwin: ['Library/Application Support/orca/daemon'],
linux: ['.config/orca/daemon'],
win32: ['AppData/Roaming/orca/daemon', 'AppData\\Roaming\\orca\\daemon']
}
const REAL_USER_DAEMON_MARKER_LIST = REAL_USER_DAEMON_MARKERS[process.platform] ?? ['orca/daemon']
export function processArgs(pid) {
try {
return execFileSync('ps', ['-p', String(pid), '-o', 'args='], {
encoding: 'utf8',
timeout: 5_000
}).trim()
} catch {
return null
}
}
export function processState(pid) {
try {
return execFileSync('ps', ['-p', String(pid), '-o', 'stat='], {
encoding: 'utf8',
timeout: 5_000
}).trim()
} catch {
return null
}
}
export function isProcessAlive(pid) {
try {
process.kill(pid, 0)
return true
} catch (error) {
return error?.code !== 'ESRCH'
}
}
// Why scan by tag rather than trust the session pid: macOS wraps the PTY in
// /usr/bin/login for TCC attribution, so the agent process is a descendant of
// the session leader — exactly as a real `claude`/`codex` launch would be.
export function findTaggedPid(tag) {
try {
const output = execFileSync('ps', ['-eo', 'pid=,args='], {
encoding: 'utf8',
timeout: 5_000
})
for (const line of output.split('\n')) {
if (line.includes(tag)) {
const pid = Number(line.trim().split(/\s+/, 1)[0])
if (Number.isInteger(pid) && pid > 0) {
return pid
}
}
}
} catch {
// ps failed; treat as not found.
}
return null
}
export function isMarkerAlive(marker) {
return processArgs(marker.pid)?.includes(marker.tag) === true
}
/**
* The session leader of a still-live marker, read now rather than remembered.
*
* Why not trust the pid captured at staging: teardown runs a minute later, and phase 1 has
* deliberately killed things in between. A remembered leader pid may by then belong to whatever
* the OS recycled it onto, and SIGKILLing that is precisely the mistake this script exists to
* demonstrate. Returns null unless the live marker still claims this leader.
*/
export function verifiedSessionLeaderPid(marker) {
if (!isMarkerAlive(marker)) {
return null
}
try {
const ppid = Number(
execFileSync('ps', ['-p', String(marker.pid), '-o', 'ppid='], {
encoding: 'utf8',
timeout: 5_000
}).trim()
)
return Number.isInteger(ppid) && ppid === marker.sessionPid ? ppid : null
} catch {
return null
}
}
// Pre-existing daemons (the user's real one above all) must be untouched by this run.
export function snapshotForeignDaemons() {
const daemons = []
try {
const output = execFileSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf8', timeout: 5_000 })
for (const line of output.split('\n')) {
if (!line.includes('daemon-entry.js')) {
continue
}
const pid = Number(line.trim().split(/\s+/, 1)[0])
if (Number.isInteger(pid) && pid > 0) {
daemons.push({
pid,
isRealUserDaemon: REAL_USER_DAEMON_MARKER_LIST.some((marker) => line.includes(marker))
})
}
}
} catch {
// ps failed; the exit check will report an empty snapshot.
}
return daemons
}
export async function waitFor(predicate, description, timeoutMs) {
const deadline = Date.now() + timeoutMs
for (;;) {
if (await predicate()) {
return
}
if (Date.now() >= deadline) {
throw new Error(`timed out waiting for ${description}`)
}
await new Promise((r) => setTimeout(r, 200))
}
}