Files
orca/config/scripts/daemon-replacement-launcher-hold-source-assertions.mjs
T
NeilandOrca 09fae31270 fix(daemon): bound the POSIX evidence read and re-verify the pid it describes
Round-six follow-ups, none destructive.

The process-table read had a deadline on Windows but not on POSIX, where the
shared reader's ps timeout does not cover queueing behind an in-flight scan —
so the one step that runs after the grace window could still outlast it.

The pid handed to that read was verified before the grace window, which is
long enough for the daemon to die and its pid to be recycled onto a shell
with children. Verify it where it is used instead, and only when there is
still something to raise: the common case now skips the identity probe
altogether, which also takes a few seconds off the worst-case launch.

Two renderer call sites killed PTYs without handling rejection. That was
harmless while an unreachable session was answered by a silent no-op; now
that it honestly rejects, pane teardown and repo removal would log an
unhandled rejection every time — exactly when the daemon is already sick.

Deliberately not taken from that review: giving the endpoint-occupied catch
the same held fallback as the failed-health path. That path arrives with
occupancy unknown or empty, so holding there would swallow a real launch
failure to protect nothing.

Splits the repro script, which had grown past the line limit, into the
sequence it proves and the two things it proves it with: process-table
inspection, and the static assertions on the launcher's hold decision.

Co-authored-by: Orca <help@stably.ai>
2026-08-11 22:04:16 -07:00

155 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
/**
* Static source assertions for the launcher's hold decision.
*
* Split from the repro script it serves: those phases prove behaviour with real processes,
* while these read `daemon-init.ts` to pin the one property real processes cannot reach —
* that the decision is taken, and returns, before anything is killed. daemon-init.ts imports
* electron, so it cannot be executed outside the app.
*/
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
const repoRoot = resolve(import.meta.dirname, '..', '..')
export function stripComments(source) {
// Blanked rather than deleted so offsets and line numbers stay true to the real file.
const blank = (text) => text.replace(/[^\n]/g, ' ')
return source
.replace(/\/\*[\s\S]*?\*\//g, blank)
.replace(/(^|[^:])(\/\/[^\n]*)/g, (_match, prefix, comment) => prefix + blank(comment))
}
/** The balanced `{...}` block starting at `braceIndex`, or null if it never closes. */
function extractBlock(source, braceIndex) {
let depth = 0
for (let i = braceIndex; i < source.length; i++) {
if (source[i] === '{') {
depth++
} else if (source[i] === '}') {
depth--
if (depth === 0) {
return { text: source.slice(braceIndex, i + 1), start: braceIndex, end: i + 1 }
}
}
}
return null
}
function lineOf(source, index) {
return source.slice(0, index).split('\n').length
}
function normalize(text) {
return text.replace(/\s+/g, ' ')
}
/**
* PHASE 3 — the launcher must hold rather than kill. daemon-init.ts imports electron, so it
* cannot be executed here; this reads the source instead, whitespace-tolerantly, and asserts
* the structural properties phase 2's inputs depend on.
*/
export function checkLauncherHoldsOccupiedDaemon({ log, assert }) {
const relativePath = 'src/main/daemon/daemon-init.ts'
const source = stripComments(readFileSync(join(repoRoot, relativePath), 'utf8'))
// 1. holdIncumbentDaemon() returns a preserved handle in 'held' mode — it does not adopt,
// which a daemon too wedged to answer listSessions could never complete anyway.
const holdDecl = source.match(/const\s+holdIncumbentDaemon\s*=\s*\([^)]*\)[^{]*\{/)
assert(holdDecl !== null, `${relativePath} does not declare holdIncumbentDaemon()`)
const holdBody = extractBlock(source, holdDecl.index + holdDecl[0].length - 1)
assert(holdBody !== null, `could not parse the holdIncumbentDaemon() body in ${relativePath}`)
assert(
/createPreservedDaemonHandle\([^)]*'held'\s*\)/.test(normalize(holdBody.text)),
`holdIncumbentDaemon() does not return createPreservedDaemonHandle(..., 'held'): ${normalize(holdBody.text)}`
)
log(
`phase 3: ${relativePath}:${lineOf(source, holdDecl.index)} holdIncumbentDaemon() = ${normalize(holdBody.text)}`
)
// 2. Process-table evidence is only ever raised from an identity-verified pid — otherwise
// it could describe a recycled pid's children rather than this daemon's terminals.
const verifiedPidCall = source.search(/readVerifiedDaemonPid\s*\(/)
const evidenceCall = source.match(/raiseOccupancyWithProcessEvidence\s*\(([^)]*)\)/)
assert(verifiedPidCall !== -1, `${relativePath} never calls readVerifiedDaemonPid()`)
assert(evidenceCall !== null, `${relativePath} never raises occupancy with process evidence`)
assert(
verifiedPidCall < evidenceCall.index,
`${relativePath} raises occupancy with process evidence before verifying the recorded pid`
)
// Whatever identifier carries the pid, its declaration must come from the verified read.
const evidencePidName = evidenceCall[1]
.split(',')[1]
?.trim()
.replace(/[^\w$]/g, '')
assert(
Boolean(evidencePidName),
`could not read the pid argument of raiseOccupancyWithProcessEvidence: ${normalize(evidenceCall[1])}`
)
const evidencePidDecl = new RegExp(
`const\\s+${evidencePidName}\\b[\\s\\S]{0,400}?readVerifiedDaemonPid\\s*\\(`
)
assert(
evidencePidDecl.test(source),
`${relativePath} passes '${evidencePidName}' to raiseOccupancyWithProcessEvidence without deriving it from readVerifiedDaemonPid — the evidence could then describe a recycled pid's children`
)
// 3. The 'occupied' branch holds and never kills.
const occupiedGuard = source.match(/if\s*\(\s*occupancy\.state\s*===\s*'occupied'\s*\)\s*\{/)
assert(occupiedGuard !== null, `${relativePath} has no 'occupancy.state === occupied' guard`)
const occupiedBlock = extractBlock(source, occupiedGuard.index + occupiedGuard[0].length - 1)
assert(occupiedBlock !== null, `could not parse the occupied branch in ${relativePath}`)
const occupiedLine = lineOf(source, occupiedGuard.index)
assert(
!occupiedBlock.text.includes('killStaleDaemon'),
`${relativePath}:${occupiedLine} calls killStaleDaemon inside the occupied branch`
)
// Holding requires BOTH: no hello ever completed, and only the process table could answer.
// A daemon that did complete a hello is adoptable, so it must not be routed to a mode that
// never adopts.
const unverifiableGuard = occupiedBlock.text.match(
/if\s*\(\s*health\s*===\s*'rejected'\s*\|\|\s*occupancy\.liveSessions\s*===\s*null\s*\)\s*\{/
)
assert(
unverifiableGuard !== null,
`${relativePath}:${occupiedLine} does not gate the hold on a daemon that cannot be adopted (rejected, or an unverifiable session count)`
)
const unverifiableBlock = extractBlock(
occupiedBlock.text,
unverifiableGuard.index + unverifiableGuard[0].length - 1
)
assert(unverifiableBlock !== null, 'could not parse the liveSessions === null branch')
assert(
normalize(unverifiableBlock.text).includes('return holdIncumbentDaemon()'),
`${relativePath}:${occupiedLine} does not return holdIncumbentDaemon() when the session count came from the process table`
)
log(
`phase 3: ${relativePath}:${occupiedLine} occupancy.state === 'occupied' + cannot-be-adopted -> return holdIncumbentDaemon(); the branch contains no kill`
)
// 4. Ordering: every kill on this path is downstream of the occupied branch, so a hold
// returns before any of them can run.
const killCalls = [...source.matchAll(/killStaleDaemon\s*\(/g)].map((match) => match.index)
assert(killCalls.length > 0, `${relativePath} never calls killStaleDaemon()`)
const killsBeforeTheDecision = killCalls.filter(
(index) => index > evidenceCall.index && index < occupiedBlock.end
)
assert(
killsBeforeTheDecision.length === 0,
`${relativePath} kills at line(s) ${killsBeforeTheDecision.map((i) => lineOf(source, i)).join(', ')}, between resolving occupancy and the hold`
)
const fallThroughKill = killCalls.find((index) => index > occupiedBlock.end)
assert(
fallThroughKill !== undefined,
`${relativePath} has no killStaleDaemon() after the occupied branch — the replacement path is gone`
)
const killLines = killCalls.map((index) => lineOf(source, index)).join(', ')
log(
`phase 3: every killStaleDaemon() call site in the file is at line(s) ${killLines} — all downstream of the occupied branch, which returns at line ${lineOf(source, occupiedBlock.start)}`
)
log(
'phase 3 RESULT: statically, the failed-health-check path resolves occupancy from a verified pid and returns a held handle before any kill. This proves the source ordering and branch contents; it does NOT execute daemon-init.ts (it imports electron), so the runtime proof stops at the inputs phase 2 produced with real processes.'
)
}