# Dormancy resource budget (D9-3 — ADR-0003 red-team #9)

<!-- [doc->REQ-INST-3] -->

**What this answers.** ADR-0003's Stage-A red-team #9: *"dormant-warm default
has real resource/security cost — measure N dormant sessions on real adapters;
define an adapter-specific warm/cold policy + resource budget before locking
warm as the default."* This document records the measurement and locks the
policy with numbers instead of a guess.

## Methodology

The env-gated harness `crates/spt-daemon/tests/budget.rs` (`SPT_BUDGET=1`,
silent no-op otherwise — CI's normal sweep never runs it):

1. binds a real (net-less) broker — the production PTY-hosting path — in a
   hermetic `SPT_HOME`;
2. spawns N idle seats through `Brain::spawn_session_pid` (one probe brain per
   seat, each labeled `budget-<i>`), exactly the shape of a **dormant** seat:
   PTY master + harness child + output ring, state-preserved and undriven;
3. settles, then samples each child's **RSS**, **handle/fd count**, and
   **cumulative CPU** twice across an idle window — the CPU delta is the idle
   burn of a warm seat;
4. samples the host process's RSS before/after spawning (the broker-side
   per-seat overhead: PTY plumbing + output rings);
5. kills every seat (the **suspended** state — only the on-disk perch record
   would remain in production) and samples the residual.

Sampling: Windows via `Get-Process` (WorkingSet64 / HandleCount /
TotalProcessorTime); Linux via `/proc/<pid>/status` VmRSS, `/proc/<pid>/fd`
count, and `/proc/<pid>/stat` utime+stime. **Limitation:** the sample covers
the direct PTY child only — a harness that forks a worker tree under-reports
by the tree's cost (the claude numbers below are the main node process, which
dominates).

Knobs: `SPT_BUDGET_N` (seats), `SPT_BUDGET_PROGRAM`/`SPT_BUDGET_ARGS` (the
seat command — point at the real adapter's harness), `SPT_BUDGET_SETTLE_MS`,
`SPT_BUDGET_IDLE_MS`. Rig runs ride the CI `[budget]` commit-message gate
(both hosts measure in one tagged push) or a direct local
`SPT_BUDGET=1 cargo test -p spt-daemon --test budget --release -- --nocapture`.

## Numbers

### HFENDULEAM (Windows 11, 2026-06-04, local run)

5 × `cmd` (shell seat — the floor):

| metric | per-seat |
|---|---|
| RSS | 7.7 MiB |
| handles | 85 |
| idle CPU (5 s window) | 0.000 s |

3 × `claude` (real adapter seat — the heavy case):

| metric | per-seat |
|---|---|
| RSS | ~308.5 MiB (307.9–308.9) |
| handles | 244 |
| idle CPU (5 s window) | 0.000 s |

Broker-side overhead: ~0.2–0.3 MiB per seat (RSS delta ÷ N). Suspended
residual: host process returns to its ~6–7 MiB baseline — a suspended seat
costs **only the on-disk perch record** (bytes, not megabytes, no handles, no
CPU).

### gravity (Ubuntu 22.04, 2026-06-04, `[budget]`-tagged CI run @ 785002e)

5 × `sh` (shell seat — no LLM harness installed on this host, so the shell
floor is its measurement; the adapter-class number generalizes from
HFENDULEAM's, the harness binary being the same node runtime there):

| metric | per-seat |
|---|---|
| RSS | 1.7 MiB |
| fds | 4 |
| idle CPU (5 s window) | 0.000 s |

Broker-side overhead: ~0.3 MiB per seat. Suspended residual: host process
back at its ~4.7 MiB baseline.

## The policy (locked by these numbers)

1. **Warm (dormant) stays the default.** An idle warm seat burns **zero CPU**
   on both adapter classes; the cost is purely resident memory. A shell-class
   seat is ~8 MiB — effectively free at any realistic seat count. A heavy
   LLM-harness seat is ~300 MiB — a workstation-class node holds ten warm
   seats in ~3 GB, acceptable for the instant-reactivation win
   (CONTEXT §dormant/suspended: wake-in-place is the point of warm).
2. **Auto-suspend stays opt-in, default OFF globally** — confirmed, not
   amended. The knob chain shipped in D9-2 (`auto_suspend_after_ms`: global
   default OFF → `daemon.json` node leg → `info.json` endpoint leg, `0` =
   explicit endpoint OFF) is the policy surface.
3. **Constrained nodes should default the node leg ON.** ~300 MiB/seat is
   real on a Pi/handheld-class node: set `auto_suspend_after_ms` in
   `daemon.json` there. The threshold counts from **dormancy onset** (the
   `dormant_since_ms` anchor), never from last activity.
4. **Heavy-harness endpoints may opt themselves out/in per seat** via the
   endpoint override — e.g. a billable harness whose warm session holds a
   cost/footprint beyond RSS (seat-count limits) sets a per-endpoint
   threshold even on a big node. Note: an **idle** claude seat holds no API
   spend (tokens flow only on input); the footprint concern there is seat
   count and memory, not per-minute billing.
5. **Suspended = cold = nearly free.** The measured residual confirms the
   suspended state's design: resume-on-wake trades latency for ~everything
   else.

ADR-0003 #9 is closed by this document (see the amendment note there).
