Skip to content

fix: probe plan survives a reboot - #37

Merged
zebraengine merged 1 commit into
mainfrom
fix/probe-state-persistence
Sep 25, 2026
Merged

zebraengine merged 1 commit into
mainfrom
fix/probe-state-persistence

Conversation

@zebraengine

Copy link
Copy Markdown
Owner

Problem

The amp controller's state (active cap, debounce streaks, probe-plan progress) lived in /tmp/derate_amp_control.state.json, the daemon's default --state-file, which the installer never overrode. The mini-PC's 2026-09-24 04:00 kernel-update reboot cleared it. The daemon then treated the plan as never started, and at that evening's session start it capped to 32A for a cold probe. The last 32A cold probe had run on 09-21, only three days earlier, the plan interval is 7 days, and 40A cold, 32A warm and 40A warm still had zero replicates. The 09-21 run was also lost from the daemon's count. The installer creates no other state, so every reboot has the same effect (the box rebooted on 09-05, 09-11 and 09-24).

Code touched

  • deploy/install-derate-amp-control.sh: the unit gets StateDirectory=derate-amp-control, and --state-file now defaults to /var/lib/derate-amp-control/state.json. An existing /tmp state file is copied there once, so the move does not drop a cap that is active mid-session. An explicit --state-file still wins.
  • contrib/derate_amp_control.py:
    • probe_history_from_events() (pure) rebuilds probes_done and last_probe_ts from amp_capped events that carry detail.probe, and from legacy events whose reason says "calibration probe" (counted as any). A probe counts as complete when the controller's next amp event, or now if there is none, comes at least a full hold after its start (60 s tolerance). That is needed because a probe that finishes on a sustainable current no higher than its own logs nothing at completion. Abandoned probes always log something sooner: a lower thermal cap, or the session-end restore. The cadence anchor is the plug-in time of the probe's session from /api/sessions, which is a few minutes before the daemon's own charging start. That can only make the next probe due sooner, never push a cold slot too late.
    • main() runs the rebuild at most once per state file (probe_history_checked), only when the plan is enabled, no hold is in progress and the history is empty. If wallmonitor cannot answer, that tick runs with the probe disabled rather than treat the plan as overdue.
    • The --state-file help now says the default is cleared on reboot.
  • Left alone: last_charging_ts is not rebuilt, so the first session after a state loss still cannot count as cold (existing, tested behavior). The daemon's own /tmp default is kept for ad-hoc runs.

Risk

  • Two extra GETs (/api/events, /api/sessions) the first time a state file is empty, then none.
  • A fresh install with no event history rebuilds an empty history and probes at the first opportunity, exactly as before.
  • Thermal capping and restoring are untouched. The fallback only switches the probe off for the tick.

Verification

  • uv run pytest: 167 passed. The 4 new tests cover: tonight's incident replayed (3 days later: no probe; 7 days later: 32A warm at minute 30, not cold again); abandoned, silent-completion, legacy and in-progress probes; the rebuild running exactly once; and the plan holding when the rebuild fails.
  • Ruff: no new findings (the 3 existing lint findings and the format drift are the same as on main).
  • Dry run of this branch on the mini-PC against the live wallmonitor, with a scratch state file: probe history rebuilt from the event log: 3 completed (one pre-plan 32A any, both 32A cold), last_probe_ts = 2026-09-24 17:50:43 (tonight's plug-in).

Deploy: re-run install-derate-amp-control.sh with the current flags. On this box, delete /tmp/derate_amp_control.state.json first (between sessions). Otherwise the installer migrates its one-entry history, the rebuild never triggers, and the count stays one short.

🤖 Generated with Claude Code

…t from the event log

The amp controller kept its state in /tmp/derate_amp_control.state.json.
The 2026-09-24 04:00 kernel-update reboot cleared it, the daemon read the
plan as never started, and that evening's session probed 32A cold again -
three days after the last one, against a weekly cadence, while three
conditions still had no replicates at all.

Two layers. The installer now runs the daemon with StateDirectory= and
--state-file /var/lib/derate-amp-control/state.json, carrying an existing
/tmp file over. And a daemon that finds its probe history empty rebuilds it
once from wallmonitor's amp events: a probe counts as completed when the
controller's next amp event (or now) comes a full hold after its start,
since a probe finishing on a sustainable current no higher than its own
logs nothing at completion, and the cadence is anchored at the session's
plug-in. If wallmonitor cannot answer, that tick runs with the probe
disabled rather than treat the plan as overdue; thermal capping is
unaffected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@zebraengine
zebraengine merged commit 5b1ca88 into main Sep 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant