Skip to content

daemon: record owned child pids so leak detection reads a record instead of reconstructing it from ps #1882

Description

@thymikee

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;
  • a startup-reaper input: the same record tells a fresh daemon what a dead predecessor left behind, which is what web: agent-browser Chrome fleets leak and can spin at 100% CPU for days — add idle lifecycle/reaper #1109's orphan cleanup infers today from Chrome command lines.

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.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions