The status bar's CPU card lists what every live session runs besides omp, and the server stops those processes on request, once their sessions have been idle long enough, and once their session leaves the roster.
SessionProcessWatch in src/server/session-processes.ts does all three, so there is one path that stops a session's processes.
omp's bash tool runs in omp's own process: each command it runs, foreground or async, is a child of omp that leads its own process group, and so do omp's Python kernels and private browsers.
Each such group is a job, listed by its top process, the group's leader, and stopped as a whole.
omp's stdio MCP servers are children of omp in omp's own process group: they are omp's helpers, not something the session started, so the card only shows their memory and CPU with omp's, and they end with the session.
omp starts its shared workers, whose commands begin with __omp_worker_, such as a project's daemon broker, the blob broker, or the LSP multiplexer, as children of whichever omp first needs them, and they serve every omp of the project or the machine.
descendants in src/process-census.ts skips them and everything under them, for the card, the keeper's census, and stopTree alike; OMP_WORKER_PREFIX names the prefix omp's cli/worker-selectors.ts gives every worker.
A service is a daemon the project's broker runs: a dev server the bash tool started with a name, or omp's shared headless browser.
It runs under the broker, not under omp, and outlives the session that started it while any omp stays in the project, which with sessions the dashboard keeps running is forever.
ompDaemons in src/omp/daemons.ts lists them with omp's own collectReports from cli/ps-data.ts, which never starts a broker, keeps the ones a live broker supervises that have not ended, and stops one generation through the broker's stop with its id and a 3-second timeout.
A service whose snapshot names an owner belongs to the session with that omp session id, and to the live session that took the id over with /new.
A service with no owner, such as the shared browser, is shared: it serves every live session whose directory resolves, through omp's canonicalProjectDir, to the broker's project.
A Docker container that a session started detached is not listed: Docker Desktop records no client process, and compose's working directory label names the main checkout even for a run from a worktree, so nothing ties a container to a session.
A foreground docker compose up or docker run is a process group under omp, so it lists and stops as any job.
The server that holds the owner lock samples every 15 seconds, whether a page listens or not, and every server samples again for each GET /api/session-processes and each stop: one ps -A -o pid=,ppid=,pgid=,rss=,pcpu=,lstart=,args=, run without blocking the server, and one collectReports.
pcpu is ps's percent of one core, which macOS reads as an average that decays over the last minute, and LC_ALL=C keeps its decimal point a dot.
Samples run one at a time.
A job's id is its process group and its top process's start time, and a service's id is its broker's runtime directory, its name, and its generation, so a stop of an id that no longer runs, or now names another process, stops nothing and answers the current list.
A job stops through terminate: SIGTERM to each member, then SIGKILL after 3 seconds to those still running under the same start time, so a reused pid is never hit.
A ps that fails fails the sample rather than reading as nothing running.
GET /api/session-processes answers a LiveSessionProcesses from src/shared/processes.ts for every live session, in roster order: its instance id, omp's own memory, CPU, and process count, and the jobs and services, biggest memory first, each with its id, kind, label, memory, CPU, process count, start, whether it is shared, and its stopsAt.
The memory and CPU of omp, a job, or a service are sums over its processes, and a shared service shows under every session it serves.
PUT /api/session-processes/stop takes { instanceId, id }, checked by parseProcessStop in src/server/wire.ts, and answers the same list after the stop; a stop for a session that no longer runs stops nothing.
GET /api/process-settings answers the ProcessSettings, and PUT /api/process-settings takes them, checked by parseProcessSettings, and answers them as saved.
Each live session has an idle clock: the time of the last sample that saw it anything but idle, so a session that waits on an answer counts as busy.
A process's idle stop time is the setting's minutes after the latest of its own start and the idle clocks of the sessions it serves, and there is none while one of them is busy.
A job and an owned service serve their session, and a shared service every live session of its project, so the shared browser stops only once all of them have been idle that long.
ProcessSettingsFile in src/server/process-settings-file.ts keeps the setting in processes.json beside the access token, as { stopIdleAfterMinutes }, one of null for off, 15, 30, 60, and 120, 30 by default, and moves a file that holds anything else to processes.json.invalid.
It is a shared ReducerFile and each sample reads it again, so a choice saved on any server reaches the one that stops processes.
The watch remembers, for each live session, its omp's pid and start time, the jobs it last saw under it, and every omp session id it had. When a session leaves the roster while its omp still runs, the watch keeps that memory, since the session may come back. Once its omp no longer runs, the watch stops the services the session owned and the jobs it last saw, which omp's exit left reparented, and forgets it. The keeper already stops a dashboard session's jobs when it ends; stopping them again by pid and start time is harmless, and it covers terminal sessions and a dashboard session's services, which run under the broker.
Only the server that holds the owner lock stops idle processes and what a gone session left, as with the rest of the unattended work; see One server runs the unattended work. Another server samples only when a page asks, lists no stop time, and still stops a process when you press Stop. A server lists omp's registry of terminal sessions again only while a page listens, so with no page open a terminal session that ended stays on the roster, and its cleanup waits, until a page opens.