# SAFETY-95 - a maintenance drain that ends hands back smoothly **Status: implemented 2026-10-06, for review.** Tests: `test_safety95.py`. **The defect** (SAFETY-93 review, O1). At N=2, when A's drain ended (A switched Off, say) with the house at 1200 W, B went 0 -> +1600 W in one cycle and exported. It is the mirror of SAFETY-93 on the drain end. **Cause.** During a drain the site target carries A's drain floor (SAFETY-78, `drain`, 2000 W) above what the house needs. At the end A's command drops (pin, Off, abort, stale link, refusal), but the loop slews from last cycle's target (2000 -> 1600) and only B is left to place it. Under a solar surplus B gave 1600 W into the export. **The rule** (`main.py`, `_reseed_drain_end()`, `_drain_grid_w()`, N>=2 only). On the cycle a drain ends: 1. The start point is what the others stood at plus A's new command (its pin, else 0 W; a dropped battery is sent 0 W), as SAFETY-93 does. 2. If the house needs more, the start is raised to the house's own point (live batteries' readings + grid - target, as `_reseed_run_edge()`), but never above last cycle's start. So the others cover the house at once and never take over the drain's export. 3. While A settles (SAFETY-96's window: within the deadband, at most `PLANT_SETTLE_DOWN_S`), the loop's error is the house's point minus its start, not the meter. The meter still shows A's tail and the others' climb; chasing it sent B down and then up again. 4. Then the loop moves one slew step a cycle. A drain ends when its battery leaves DRAIN, is not live, its claims contradict, or it will not give (SAFETY-73's limit outranks the pin without a contradiction, so the refusal is read from the battery; +1 only, as `_held_back()`). Today a contradiction on a drain only arises through a refusal; the clause stays because a contradiction sends A 0 W, which is an end (tested directly). The end is kept until the loop runs (a missed poll). A dropped draining battery is this rule's, not `_reseed_drop()`'s. Order in one cycle: drop/rejoin (SAFETY-96), charge end (SAFETY-93), drain end (this) last; each moves the start by its own battery, and the drain end's raise reads the start the others left. **Why not SAFETY-93's rule alone** (step 1, then slew). Measured on the same plant: at 1200 W the grid imports 849 W and B goes to -465 W (charging) while the loop chases A's tail; at N=3 and 4000 W it imports 784 W where base imports 42 W, as the others give up A's share of the house. The house point keeps the grid at its aim; the cap at last cycle's start keeps it from exporting. **Before -> after** (`test_safety95.py`, 2500 W batteries, 10 s lag, 2 s meter; base = the same tree with the change taken out): | case | before | after | |---|---|---| | drain done / Off / timeout, N=2, 1200 W | B 0 -> 1600, +390 W commanded export, grid then +559 W | B 0 -> 1210, grid within 40 W of its aim | | the same, N=3, 1200 W | B, C 0 -> 800, grid +695 W | 0 -> 605, grid +39 W | | the same, solar surplus | B gives 1600 W into the surplus | B never gives; site moves 400 W a cycle | | refusal, N=3, 1200 W | B, C step 1020 W, +2000 W commanded export | step 20 W, +70 W | | N=3, 4000 W | others take A's share at once | the same (grid +1 W, base +42 W) | | abort (site zeroed by the operator) | climbs from 0, grid +669 W | climbs from 0, grid +16 W | | stale link (board watchdog 25 s) | | as base, but N=3 4000 W: grid +676 W (base +786) | | N=1, every drain end | | byte-identical | `tools/opt11_identity.py` n2_maint moved on purpose (A's drain refused at 1655 s; B stays at the house's 1420 W instead of 2500 W). Re-pinned in `test_modes.py`. The other 45 digests are byte-identical. **Not changed.** The peak guard's floor still binds (arbiter, after the loop). Held batteries still give last; the idle clamp and the freeze test are as they were. A charge end (SAFETY-93) and a trade end are unchanged.