# Adopting an already-running process `job adopt --pid N` makes the broker aware of a process that was started **outside** jobd (a `nohup python train.py &`, a tmux pane, a notebook kernel). The process becomes a job: it shows in `job list`, holds a worker slot and its resources, can be depended on, waited on and cancelled — and jobd stops treating its GPU memory as anonymous foreign load. Linux only. It needs `/proc`; anywhere else `job adopt` refuses. ```bash # on the host where the process lives job adopt --pid 41233 --project sdxl --gpu # or --vram-gb 16 job submit --project sdxl --gpu --depends-on --depends-on-any-exit -- python eval.py ``` ## How it works 1. **CLI, on the process's host**, reads `/proc/`: the start time (`stat` field 22, clock ticks since boot), `cmdline` and `cwd`. It sends them to `POST /adopt` with the worker host name (`--host`, default `$JOBD_WORKER_HOST` or the hostname — the same rule the worker uses). 2. **Broker** refuses (409) unless that worker is online and advertises the `jobd-adopt` tag — a worker too old to understand adoption must not be handed one. Otherwise it inserts a job row **directly in `running`**, with `worker = host`, `adopt_pid`, `adopt_start_ticks`, `preemptible = false`, no retries, no timeouts, and emits `job_adopted`. 3. **Worker** learns of it from the `/heartbeat` response, which now lists the host's running adopted jobs (`adopted: [...]`). It launches nothing. It opens a pidfd (`os.pidfd_open`), *then* compares the start time, so the fd is known to refer to the process the user named; without pidfd support it polls `/proc//stat` and compares the start time on every look. A PID whose start time differs is a different process — PID reuse — and is refused. 4. The job stays `running` while the process lives and goes terminal when it exits. The adopted job is never `queued` or `assigned`, so none of the requeue paths (sweeper reclaim, reconcile, admission refusal, retry) can ever see it, and `/next-job` additionally refuses to claim any row carrying `adopt_pid`. The invariant: **jobd never executes an adopted job's recorded command** — it is a label read from `/proc`, not something to run. Because delivery rides on the heartbeat, a restarted worker re-learns its adopted jobs within one heartbeat and resumes watching them; the start-time check makes that safe across reboots. If the worker stays down past the dead-worker window the job is `orphaned` (`worker_died`) like any other, and is not picked up again when the worker returns; the process is untouched, so adopt it again. ## What cannot be known - **The exit code.** Only a parent can `wait()` a process. jobd did not fork it, so how it ended is unobservable. An adopted job whose process exits ends **`orphaned`** with `termination_reason = adopted_exit_unobserved` and `exit_code = null` — "we lost sight of the outcome", which is the truth. It is never reported `completed`. Consequence: default-policy dependents are cascade-cancelled; "run this after it ends" is `--depends-on ID --depends-on-any-exit`. For the same reason `job wait` / `job status` on it exit 1, not 0. - **Its output.** stdout/stderr belong to whoever started it. `job logs` is empty for an adopted job. - **Its start.** `started_at` is the adoption time, not the process's. - **Its children's VRAM, fully.** The adopted PID and its live descendants count as jobd-owned. A child that daemonised away from the tree still reads as unregistered. ## Refusals (all loud, all terminal `failed`) | `termination_reason` | Meaning | |---|---| | `adopt_pid_gone` | The worker found no such PID (exited already, or wrong `--host`). | | `adopt_pid_mismatch` | The PID exists with a different start time: reused, or another machine's PID. | | `adopt_uid_mismatch` | The process is not owned by the worker's user. The worker could not signal it, so a `job cancel` would be a lie; it refuses to adopt rather than adopt something it cannot cancel. | ## Cancel `job cancel ID` sends the process SIGTERM, then SIGKILL after the job's grace (60 s). Only that PID is signalled — never its process group, which may be the user's interactive shell. Signals go through the pidfd, so they cannot land on a recycled PID. The job ends `cancelled`. `job preempt` is refused (adopted jobs are not preemptible); a worker shutdown/drain leaves the process alone. ## VRAM accounting Before adoption the process's VRAM is reported as `unregistered_vram_gb`, which the matcher subtracts on top of NVML's already-reduced free figure. After adoption the PID (and descendants) join the worker's owned set, exactly as a launched job's PIDs do, so that memory leaves `unregistered_vram_gb`. The job's declared `vram_gb` reserves only what the process does not already hold (`max(0, declared − held)`, the existing in-flight rule), so nothing is counted twice. The adopted job occupies a worker slot; adopting onto a full worker is allowed (the process exists regardless) and simply keeps the worker from claiming more until a slot frees. ## Not in scope No MCP tool (adoption must run on the process's host; the MCP server may not), no log capture, no adoption of process groups or containers, no non-Linux.