You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split out of #1781 B1 (#1859), which made the asymmetry concrete.
The asymmetry
The daemon already writes a durable record for the resources it owns except processes:
screen recordings and app logs get a durable capture descriptor (sessions/<name>/*.resource.json, ADR 0019) carrying lifecycle, an ownership fence, and — for the simulator backend — the recorder's pid and start time inside descriptor.body.processes;
device claims, runner leases and the shutdown report are all files with owner identity.
There is no equivalent for "processes this daemon spawned and still owns". runCmdBackground children (the simctl io … recordVideo recorder), the managed agent-browser daemon and its Chrome fleet, and Apple runner xcodebuild processes are all owned in practice and recorded nowhere.
Why it costs
#1859 needed to answer "did this daemon leave a process behind?" and, with no record to read, had to reconstruct ownership from the OS four different ways: PPID descendant, PGID = daemon pid (children reparented to launchd keep the group), AGENT_DEVICE_STATE_DIR inherited in the environment, and the state dir as an argv token. Each rule exists because the others miss a real case — the #1324 recorder is caught only by pgid, the #1109 agent-browser fleet only by the inherited environment.
That is ~220 lines of heuristics that cannot run as a lane assertion: the three real-subprocess daemon lanes are device-free, so their daemons own no children and the check compares an empty set to an empty set. It lives in test/integration/support/daemon-owned-process-probe.ts as a manual red-proof script, with a fixture test as its only CI guard.
The heuristics are also approximations. A pgid is reserved only while the group has a live member, so on a long-lived host pid reuse can hand the number to an unrelated process; macOS hides the environment of Apple platform binaries from ps -E, so simctl is invisible to the env rule.
What to do
Give owned child processes the same treatment as captures: when the daemon spawns a process it owns, record { pid, startTime, command, purpose } under the owning session (or the daemon root, for daemon-scoped children like the managed browser), and clear the entry when it is reaped. The identity primitives already exist — readProcessStartTime / readHostProcessIdentityObservations (src/utils/host-process.ts) are what the recorder descriptor already uses to prove a pid is still the process it was.
Then leak detection becomes "read the record, assert every entry is dead", which is:
shorter — one rule instead of four, no ps reconstruction, no environment inspection;
stronger — it catches a leaked child the heuristics miss (one that setsids away and whose argv/env carry no state-dir path), and it cannot false-positive on a pid-reused group;
assertable in every lane, including the device-free ones, because an empty record is a meaningful assertion rather than a vacuous one;
#1431 wants exactly this as its observability half: injected faults are only meaningful if the resulting leak is observable. #1859 landed the arms that read a record (surviving daemon, unfinalized capture handle, state-dir residue); this issue is what lets the process arm join them.
Acceptance: an owned-child record exists and is written/cleared on the spawn and reap paths; daemon-owned-process-probe.ts and its fixture test are deleted in favour of a record-reading rule inside daemon-leak-model.ts; the #1324 and #1109 scenarios still go red against their pre-fix behaviour with the new rule.
Split out of #1781 B1 (#1859), which made the asymmetry concrete.
The asymmetry
The daemon already writes a durable record for the resources it owns except processes:
sessions/<name>/*.resource.json, ADR 0019) carryinglifecycle, an ownership fence, and — for the simulator backend — the recorder's pid and start time insidedescriptor.body.processes;There is no equivalent for "processes this daemon spawned and still owns".
runCmdBackgroundchildren (thesimctl io … recordVideorecorder), the managed agent-browser daemon and its Chrome fleet, and Apple runnerxcodebuildprocesses are all owned in practice and recorded nowhere.Why it costs
#1859 needed to answer "did this daemon leave a process behind?" and, with no record to read, had to reconstruct ownership from the OS four different ways: PPID descendant, PGID = daemon pid (children reparented to launchd keep the group),
AGENT_DEVICE_STATE_DIRinherited in the environment, and the state dir as an argv token. Each rule exists because the others miss a real case — the #1324 recorder is caught only by pgid, the #1109 agent-browser fleet only by the inherited environment.That is ~220 lines of heuristics that cannot run as a lane assertion: the three real-subprocess daemon lanes are device-free, so their daemons own no children and the check compares an empty set to an empty set. It lives in
test/integration/support/daemon-owned-process-probe.tsas a manual red-proof script, with a fixture test as its only CI guard.The heuristics are also approximations. A pgid is reserved only while the group has a live member, so on a long-lived host pid reuse can hand the number to an unrelated process; macOS hides the environment of Apple platform binaries from
ps -E, so simctl is invisible to the env rule.What to do
Give owned child processes the same treatment as captures: when the daemon spawns a process it owns, record
{ pid, startTime, command, purpose }under the owning session (or the daemon root, for daemon-scoped children like the managed browser), and clear the entry when it is reaped. The identity primitives already exist —readProcessStartTime/readHostProcessIdentityObservations(src/utils/host-process.ts) are what the recorder descriptor already uses to prove a pid is still the process it was.Then leak detection becomes "read the record, assert every entry is dead", which is:
psreconstruction, no environment inspection;Relationship
#1431 wants exactly this as its observability half: injected faults are only meaningful if the resulting leak is observable. #1859 landed the arms that read a record (surviving daemon, unfinalized capture handle, state-dir residue); this issue is what lets the process arm join them.
Acceptance: an owned-child record exists and is written/cleared on the spawn and reap paths;
daemon-owned-process-probe.tsand its fixture test are deleted in favour of a record-reading rule insidedaemon-leak-model.ts; the #1324 and #1109 scenarios still go red against their pre-fix behaviour with the new rule.