--- name: daemon description: Submit, monitor, and manage GPU jobs through the anima daemon (make daemon-*, make gen, make run-status, MCP bridge, discovery). Load before launching any GPU process as an agent, checking training-run progress, batch-generating images, or wiring a new daemon client. --- # Daemon job queue & GPU work Local FIFO job queue (`anima_daemon/`), auto-starts on first submit. Full HTTP contract: `anima_daemon/README.md`. **Agent-launched GPU work must go through the daemon.** A GPU process started from a Claude Code background Bash gets killed by the harness sandbox layer after ~1 min (silent SIGKILL — no OS/OOM trace, no traceback; observed 2026-07-25). Daemon jobs also queue behind a live train run instead of OOM-colliding, and survive the terminal. ## Targets `make daemon | daemon-run ARGS=" …" | daemon-wait [JOB=] | daemon-attach [JOB=] | daemon-jobs | daemon-log [JOB=] | daemon-pause [JOB=] | daemon-resume [JOB=] | daemon-kill | daemon-terminate | daemon-prune` - Front door: **`make daemon-run ARGS=" [flags]"`** — attach-by-default, exits with the job's code; `--queue` detaches, `--inline` bypasses the daemon; `--stall-timeout S` where `0` = off. daemon-run's own `--label`/`--stall-timeout` go **before** the script path — after it every token reaches the child untouched (bench scripts take `--label` themselves), and `--` passes everything after it verbatim. - **`make daemon-wait [JOB=]`** blocks to terminal and prints the record + result envelope, exiting with the job's code (`DaemonClient.wait()` programmatically) — don't hand-roll an HTTP poll loop. - `daemon-pause` tree-freezes the running job (SIGSTOP — VRAM held, SM idle, resume instant; the queue does NOT advance past it; refuses `accelerate launch` runs). `daemon-pause RELEASE=1` is the cooperative variant for train.py jobs: the trainer saves a resumable state at its next optimizer step and exits (`run_end paused`), the GPU is freed and the queue advances; the job parks as `paused` + `released` and `daemon-resume` re-enqueues it at the front with `--resume ` (reload + recompile, not instant). Protocol: `pause.request` / `pause.ack.json` in the job dir, `library/training/pause.py`. - Append `--queue` to any train/distill target to enqueue instead of running inline (`make lora --queue`, `make turbo --queue`). GUI Train button, ComfyUI trainer node, and preprocessing all submit here. - Long-quiet phases: prefer `--stall-timeout` over a heartbeat, else `bench/_common.py::start_heartbeat()` (the watchdog also spares a quiet-but-CPU-burning tree). ## Job environment A job's env is **daemon-env ← `captured_env` ← `extra_env`**. `captured_env` is the submitter's `ANIMA_*` / `CUDA_*` / `HF_*` / `PYTORCH_*` / `TORCH_*` / `NCCL_*` at submit time (recorded in `job.json`); everything else — and every whitelisted var the submit shell does **not** set — comes from the shell that booted the daemon, possibly days ago (a stale-code respawn re-boots it from whichever shell submitted next). - An unset var cannot override: if the daemon booted with `ANIMA_VOCAB_PACK=`, a later submit without the var trains on the preview pack. Set every env lever the job depends on in the submit shell, and confirm it from the job itself: `captured_env` in `job.json`, the value in `/proc//environ`, or the line the job logs (the vocab pack logs its sha). - **`make daemon-terminate` is the reset, and it is cheap when nothing is running** — all state is on disk and the next submit boots a fresh daemon from the current shell. Use it whenever `make daemon-jobs ARGS="--state queued,running,paused"` reads `0 of M` and the daemon's env is in doubt (an old session booted it, an env lever changed, a run is about to take hours). It kills the active job and discards the queue, so check that line first. ## Reading the queue | question | command | what comes back | |---|---|---| | Is a job running / is the queue busy? | `make daemon-jobs ARGS="--state queued,running,paused"` | one line per unfinished job, then `N of M jobs` — a bare `0 of M` **is** the "nothing running" answer (exit 1 = daemon down) | | How far along is the current run? | `make run-status` | `step N/total`, it/s, ETA, last losses, last ckpt (§ Run status below) | | Did job `` finish, and with what exit code? | `make daemon-jobs ARGS="--all" \| grep ` | one line: when · id · state · `rc=` · duration · target · first error line | | …and block until it does? | `make daemon-wait JOB=` | exits with the job's own code (record + envelope on stdout) | | What argv did job `` run? | `python -c "import json;print(' '.join(json.load(open('output/daemon/jobs//job.json'))['argv']))"` | the child argv on one line. **Command jobs only** — a train job persists `method`/`preset`/`overrides`/`extra` and builds its launch cmd at spawn, so its `argv` is empty | | One job's full record + its bench `result.json`? | `python -m anima_daemon status ` | the whole record, envelope inlined under `result`; reads the on-disk `job.json` when the daemon is down | | Is the daemon up, on which port, running stale code? | `python -m anima_daemon status` | `up`, resolved `base_url`, `stale_code`, `paused`, `active_job` (exit 1 when down) | `daemon-jobs`, `daemon-log`, `run-status` and the `job.json` read all work with the daemon down. `daemon-jobs` prints **oldest first** (`| tail -5` = the five most recent), capped at 15; filter with `ARGS="--running|--failed|--done|--state s[,s]|--limit N|--all"`. Jobs do not always start in submit order (a chained job waits on its parent), so ask for pending work by state rather than trusting the newest-15 slice to contain it. `make daemon-log [JOB=]` dumps a job's stdout from disk (`ARGS="-n 200"`; `-n 0` = all); `daemon-attach` follows a *live* stream only, so it has nothing for a finished job. ## Discovery & agent surface - Discovery is pidfile-based: `output/daemon/daemon.json` / `~/.anima/daemon.json` → `{port, root}`. **Never hardcode 8765** — the port falls back to ephemeral on collision. - `python -m anima_daemon submit|wait|status` is the stdlib-only equivalent of the `make` targets, for callers that can't import `tasks.py`. - The daemon self-describes at `GET /` (README) and `GET /tools` (JSON-Schema manifest). `anima_daemon/mcp.py` is a stdio MCP bridge over the same surface — register the script path as the MCP command; it discovers the daemon itself. - Job dirs are retention-bounded. `make daemon-prune` is the manual sweep — dry-run unless `ARGS="--apply"`. Rules and knobs: `anima_daemon/README.md` § Retention. ## Batch generation: `make gen` Daemon-routed batch generation — same argv + env levers as `make test`, submitted as a GPU command job (attach-by-default; `--queue` detaches, `--inline` bypasses). Lands a `gen_manifest.json` in the job record: `write_gen_manifest` drops a `result_path.json` pointer when the daemon exports `ANIMA_DAEMON_JOB_DIR` (a plain `python inference.py` is unaffected). ## Run status: `make run-status` `step N/total`, it/s, ETA, last losses, last ckpt, and `RUNNING`/`OK`/`ERROR`/`DEAD` (no `run_end` + dead pid), digested from the run's `progress.jsonl` (`library/training/progress.py::read_status` — importable; `scripts/run_status.py` is the CLI). Covers train.py methods **and** `make turbo`. **Both launch paths are scanned**: an inline run's `output/logs/.progress.jsonl` *and* a daemon job's `output/daemon/jobs//progress.jsonl` (the daemon overrides `--progress_jsonl` with a per-job path, so the run dir under `output/logs/` holds the snapshot + TB events but no stream). Defaults to the newest stream from either; `RUN=` selects — a daemon stream's filename is bare, so a run name there is matched against the `run_start` event inside it, and the header prints `(job )`. `ARGS="--list"` for all, `ARGS="--json"` for the dict, `ARGS="--jobs-dir ''"` to skip the daemon dirs. For every scalar instead: `make export-logs RUN=output/logs/ SUMMARY=1` prints max-step + last value per tag (raw payload `{"run", "tags": {tag: [[step, wall_time, value], …]}}` — value is `row[2]`).