fix(daemon): do not read a stranded login wrapper as a hosted terminal

Raised by a colleague's handoff on the same-day reports. macOS wraps every
terminal in /usr/bin/login for TCC attribution, and #13764 shows the wrapper
can outlive the shell it wrapped — leaving a session leader that hosts
nothing. One affected host had accumulated enough of them to reach swap
pressure.

That is exactly the evidence this change treats as proof of live work, so a
daemon whose sessions had all ended would have been held indefinitely on the
strength of the corpses, on precisely the hosts where the problem is worst.
Same class as the daemon's own probe PTYs: a session leader is necessary
evidence, not sufficient. A wrapper still doing its job has the shell it
exec'd beneath it.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-11 23:05:07 -07:00
co-authored by Orca
parent c0adc29087
commit ea7033ef6e
2 changed files with 46 additions and 1 deletions
@@ -163,6 +163,35 @@ describe('inspectDaemonPtyOwnership on POSIX', () => {
).resolves.toBe('owns-live-ptys')
})
it('ignores a login wrapper stranded without its shell (#13764)', async () => {
// Why: the macOS TCC wrapper can outlive the shell it wrapped, leaving a session leader
// hosting nothing. On hosts where those accumulate, counting them would hold a daemon
// whose sessions have all ended — indefinitely, and for no live work.
await expect(
inspectDaemonPtyOwnership(DAEMON_PID, {
platform: 'darwin',
readPosixProcessTable: posixTable([
daemonRow,
row(101, DAEMON_PID, { command: '/usr/bin/login -flpq nwparker /bin/bash …' }),
row(102, DAEMON_PID, { command: '/usr/bin/login -flpq nwparker /bin/bash …' })
])
})
).resolves.toBe('no-live-ptys')
})
it('still counts a login wrapper that has its shell', async () => {
await expect(
inspectDaemonPtyOwnership(DAEMON_PID, {
platform: 'darwin',
readPosixProcessTable: posixTable([
daemonRow,
row(101, DAEMON_PID, { command: '/usr/bin/login -flpq nwparker /bin/bash …' }),
row(202, 101, { command: '/opt/homebrew/bin/bash --rcfile …' })
])
})
).resolves.toBe('owns-live-ptys')
})
it('reports unknown when the table never contained the daemon', async () => {
// Why: an unobserved root yields the same empty result as a childless one —
// reading that as "empty" authorizes killing a daemon full of live agents.
+17 -1
View File
@@ -49,6 +49,9 @@ export const WINDOWS_OWNERSHIP_PROBE_DEADLINE_MS = 6_000
*/
export const POSIX_OWNERSHIP_PROBE_DEADLINE_MS = 4_000
/** macos-tcc-login-shell.ts wraps every darwin terminal in this. */
const MACOS_LOGIN_WRAPPER_PREFIX = '/usr/bin/login '
function withDeadline<T>(work: Promise<T>, deadlineMs: number, onDeadline: T): Promise<T> {
return new Promise<T>((resolve) => {
const timer = setTimeout(() => resolve(onDeadline), deadlineMs)
@@ -111,6 +114,16 @@ function isLivePtySessionLeader(row: ProcessTableRow): boolean {
return !row.stat.startsWith('Z') && row.stat.includes('s') && !isDaemonSelfSpawnedPty(row)
}
/**
* macOS wraps every terminal in `/usr/bin/login` for TCC attribution, and the wrapper can
* outlive the shell it wrapped (#13764) — a session leader hosting nothing. Counting those
* would hold a daemon whose sessions have all ended, on hosts where they accumulate by the
* hundred. A wrapper still doing its job always has the shell it exec'd beneath it.
*/
function isStrandedLoginWrapper(row: ProcessTableRow, hasChildren: boolean): boolean {
return !hasChildren && row.command.trim().startsWith(MACOS_LOGIN_WRAPPER_PREFIX)
}
function collectLivePtyDescendants(rows: ProcessTableRow[], rootPid: number): ProcessTableRow[] {
const childrenByPpid = new Map<number, ProcessTableRow[]>()
for (const row of rows) {
@@ -135,7 +148,10 @@ function collectLivePtyDescendants(rows: ProcessTableRow[], rootPid: number): Pr
}
visited.add(child.pid)
queue.push(child.pid)
if (isLivePtySessionLeader(child)) {
if (
isLivePtySessionLeader(child) &&
!isStrandedLoginWrapper(child, childrenByPpid.has(child.pid))
) {
live.push(child)
}
}