Repository navigation
fix(mpc): show when forecast caution is off, and fix the #1644 review findings - #1663
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52f420ebc1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func (s *Service) repeatsPlanningFailureLocked(stage, message string) bool { | ||
| f := s.lastPlanningFailure | ||
| return f != nil && !s.planningFailureRestored && f.Stage == stage && f.Error == message |
There was a problem hiding this comment.
Retain new inputs when deduplicating failures
When consecutive replans fail with the same stage and message but different prices, forecasts, goals, or optimizer payloads, this predicate suppresses the newer record solely because the strings match. PlanningFailureSnapshot and the persisted evidence then continue exposing the first attempt's generation, timestamp, and inputs—even after a successful replan occurred between failures—so operators investigate stale evidence rather than the current failure. Deduplicate only when the captured inputs/payload are also unchanged, or update the in-memory latest record while separately avoiding redundant storage writes.
AGENTS.md reference: AGENTS.md:L22-L23
Useful? React with 👍 / 👎.
runReplan now tells the forecast archive which published loads Core replaced. The archive used to infer it by comparing the published loads with the selected forecast, which did not yet carry the live meter's in-flight load, so slot 0 was tagged core_recovery on every replan with a valid meter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the planning style asks for caution and the plan uses the point forecast instead, the published plan now carries caution_off (HTTP and diagnostic) and cautionOff (the app's plan message), set again on every replan and cleared once caution works. A safety_k of 0 never reads as off. Each change is logged once. Also from the #1644 review: - corePlanWithForecastRecovery returns its error last - a planning failure that repeats keeps its first record, so a lasting caution fault no longer costs a FULL-sync write per replan; a hard failure that returns after a recovered plan is current again - sanitizeSlotLoads had no caller outside its test - the Energyplan budget comment described the old single PV margin - worker tests from #1644 are renamed so make native-solver-test runs them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a car plugged in, Energyplan spends its whole budget on each solve, so caution doubled replan time: 1.5 s to 3.0 s for a 35-hour horizon and 5 s to 10 s for 48 hours. When time remains for both passes, the caution plan is now solved first. A full cautious plan that misses no departure leaves nothing for a point plan to improve on, so it is published after one solve. Otherwise the point plan is solved and compared as before. Near the end of a slot the point plan still goes first. BenchmarkNativeCautionReplan measures it on the bundled worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The point-load replay refused nearly every cautious plan with a car: the plan discharges the battery for the extra house load while the car charges, and with that load gone the discharge fed the car or exported. Dispatch already stops discharge at the house when battery_covers_ev is off, and with it on the owner lets the battery feed the car. The replay now judges only the mode's export rule and grid limits; the vehicle rules stay on the cautious plan's own inputs. From the review of #1663: - the caution budget is taken after the point solve, so it ends in the slot - caution first leaves the point plan a full fleet budget - a shortened point plan reads as caution off - only a downside planner loses caution to a prior - a repeated failure compares its message without digits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52f420e to
0a00b75
Compare
Follow-ups to #1644, from the review. Fredrik decided that publishing the point-forecast plan when forecast caution fails is fine, but only with a visible warning. This PR adds that warning, makes it show only for real fallbacks, and fixes what the review found.
Visible warning:
caution_offWhen the planning style asks for caution (safety_k not 0) and the plan uses the point forecast instead, the published plan says so:
mpc.Plan.CautionOff, sent ascaution_offinGET /api/mpc/planand in the plan diagnostic.cautionOffin the app'splanmessage (protocol.Plan). The plan feed from feat(appproto): push the plan to streaming phones when the box replans #1643 pushes it to phones when it changes, because each change comes with a new plan. An older app ignores it.It is set when the caution inputs are invalid, when Core has to drop the caution forecast as invalid, when a cautious plan breaks the point plan's limits or departure goals, when a load prior replaces the forecast on a planner that solves caution, and when the point plan covers only part of the horizon. The next plan that keeps its caution clears it. A safety_k of 0 ("bold") never reads as caution off.
Registry: no change. Plan fields (
stale,ceilingW,chargers,socPermille) live inprotocol/messages.goandmessages.ts, not inregistry.yaml, andcautionOfffollows them.Why cautious plans were refused with a car
Before this PR, Energyplan's cautious plan was refused in almost every replan with a car:
battery discharge feeds loadpoint carloadpoint car charges during battery-driven exportThe cautious forecast adds house load, and the plan discharges the battery to cover it while the car charges.
validateMarginPointLoadreplays the plan with the point load and the same battery power, so the energy the house no longer uses either feeds the car or exports. The vehicle rules then refused the plan, the point plan was published, and the badge would have shown on most car evenings.Neither refusal protects anything at dispatch:
control/dispatch.go: the energy path'suncoveredEVWclamp and the legacy path'sgridW -= uncoveredEVChargingW()). Load that does not show up cannot make the battery feed the car.So the replay now checks only what invented load could really buy: battery export the mode forbids (
modeAllows) and grid limits. The vehicle rules still apply in full to the cautious plan against its own inputs, and to every other plan.ValidatePlanis unchanged; the replay callsvalidatePlan(…, vehicleRules: false).TestForecastMarginCannotGrantExtraDischargePermissionnow covers both settings: a protected car and a covered car with export allowed are accepted, and self-consumption or passive-arbitrage export is still refused.S4: slot 0 was tagged as Core's recovery
Confirmed first:
TestForecastArchiveKeepsModelSourceUnderLiveLoadruns a real replan with the forecast tracker, varying loads and a live meter. On master it fails:slot 0 attributed to recovery: source="core_recovery" quality="fallback" model=<nil>.Fix:
runReplanpasses the archive an explicit mask of the loads it replaced (ForecastInputs.Record(base, planning, recoveredLoad, …)). The mask compares the published point loads with the selected forecast after the live overlay, so the meter's in-flight load is not a recovery. Core never recovers PV, so the PV branch is gone.S5: replan time with caution on and off
BenchmarkNativeCautionReplanrunsService.Replanend to end with the bundled Energyplan worker (ftw-solver-darwin-arm64, production budgets) and with the Core planner. Fixed instances: 10 kWh battery, 11 kW car from 40 % to 80 % in 7 hours, 17.25 kW fuse, 15-minute prices, PV, caution margin; a morning and an evening plug-in. Median of 5 replans on an Apple M5, ms to a published plan, with the car:After this PR no cautious plan was refused in any case (0 of 5 each).
¹ The worker stops before its budget once it proves a plan optimal, so a row can vary between runs; earlier runs gave 5 004 here. The final run started with a load average of 15 on 10 cores from another session.
² Measured with caution first but the old replay.
³ Not measured before this PR. There every k 0.3 replan with a car solved the point plan and then the cautious one.
With a car, Energyplan spends its whole budget on each solve (1.5 s, or 5 s at 193 slots × 5 assets), so caution doubled the time to a plan. That time is set by the budget, not the CPU, so the N150 and the Pi 4 should see the same seconds. I did not run on them; I was asked not to touch the home box during this work. Battery-only solves take 2–4 ms, and Core plans take under 3 ms with or without caution.
Change: when there is time for both passes (12 s left in the slot and in the caller's deadline), the caution plan is solved first, with a budget that leaves the point plan a full 5 s. A full cautious plan that misses no departure leaves nothing for a point plan to improve on, because
preservesDepartureGoalsaccepts it against any point plan, so it is published after one solve. Otherwise the point plan is solved and compared as before. Near the end of a slot the point plan goes first, and caution gets only the time left after it. The worker keeps no state between requests, so the order does not change what it finds. A refused cautious plan still costs two solves.I chose this over publishing the point plan first and then the cautious one, because that would show phones a point plan for a few seconds on every replan, need a second decision ID and archive record per replan, and need a second publish path in
runReplan.From the review of this PR
TestForecastCautionBudgetEndsInsideTheSlotfails without this: after a 300 ms point solve, caution could run 300 ms past the slot.withoutCaution, in both orders.downsidePrimary) reads as caution off.Earlier review notes
corePlanWithForecastRecoveryreturns(forecastChoice, error), so the error is last.sanitizeSlotLoadshad no caller outside its test and is removed. The Energyplan budget comment describes the two requests.Also found
Six worker tests never ran in CI:
make native-solver-testselects-run 'Native|RustForecastHost', and their names had no "Native". Five came with #1644 (fourTestForecastMargin…tests andTestEVGoalPlanAndPriceContract). They are renamedTestNative…and pass with the bundled worker.Tests
go test -p 2for./internal/mpc(also withFTW_NATIVE_SOLVERset),./cmd/ftw,./internal/control,./internal/appproto,./internal/api,./protocol/...node --test web/plan-brief.test.mjsmake verifyTestForecastArchiveKeepsModelSourceUnderLiveLoad,TestRecordMarksOnlyRecoveredLoads,TestCautionOffIsPublishedAndClears,TestRepeatedPlanningFailureIsWrittenOnce,TestRepeatedFailureIgnoresChangingNumbers,TestForecastCautionSolvesFirstWhenBothPassesFit,TestForecastCautionBudgetEndsInsideTheSlot,TestShortenedPointPlanIsWithoutCaution,TestPriorRecoveryMarksCautionOffOnlyForDownsidePlanners,TestPlanSaysWhenForecastCautionIsOff, clearing on the Energyplan path inTestNativeForecastMarginKeepsPointAfterOptionalFailure, acaution_offcase in the plan brief tests, and the benchmark. The budget, shortened-plan and prior-recovery tests each fail with their fix removed.Follow-ups, not in this PR
cautionOffin ftw-webapp and ftw-app, and decide whether it belongs inevents.get(review item 8).🤖 Generated with Claude Code