Adopt the Msg::Option::reject() contract in the Track B dynamic-option
handlers, which predate it. Without this, a rejected setconfig for an
xrebalance-family option was still acknowledged as success, so
lightningd persisted the never-applied value to config.setconfig --
including the junk-string case that then fails lightningd's own option
parse on the NEXT start. Every reject branch in XRebalancer,
XRebalancePredictor (via its bad_option choke point, which now names
the specific constraint instead of 'could not parse value'),
XRebalanceHistory and XMoveFunds now reports a quote-free reason.
While in the handlers, close the same parse holes Track A's review
fixed plus a NaN family specific to these stod-based knobs:
- XRebalanceHistory and XRebalancePredictor::parse_u64 parsed via
std::stoull, which accepts a leading minus and wraps modulo 2^64:
setconfig -1 became ~1.8e19. Signed parse + negative reject,
matching the FundsMover/AskreneUpdates handlers.
- std::stod accepts 'nan' and 'inf'. A NaN slips through every
range/clamping comparison (all false) straight into the stored
setting -- a NaN per_hour breaks the cycle timing, a NaN or inf
part-wait-secs breaks the part wait, and casting non-finite to
uint32 (maxparts) is undefined. parse_double and all direct stod
sites now require finite values, and maxparts clamps to
[1, 1000000] before its uint32 cast.
Phase 2 of the history+prediction design: the module that closes the
loop from observation to synthetic re-assertion. After each hourly
askrene-age pass over the clboss-xrebalance layer, XMoveFunds now
raises Msg::XRebalanceLayerAged carrying the aging cutoff (on the
failure path too -- a skipped trim only leaves stale entries, which
is safe). The new Boss::Mod::XRebalancePredictor subscribes, reads
the XRebalanceHistory observation store, runs the pure regime-walk
algorithm per channel direction, and re-asserts the surviving
walls/floors into the routed layer via askrene-inform-channel --
but only for directions whose newest real observation predates the
cutoff: directions with live evidence need no synthesis. Synthetic
assertions are never recorded back into the observation store (no
self-confirmation).
OFF BY DEFAULT. The master switch is the dynamic option
clboss-xrebalance-predict-horizon-max-secs (0 = disabled, the
default; 86400 is the intended enabled value -- and since an
asserted wall is never contradicted by routing, this cap IS the
wall re-test schedule). The other constants are dynamic options
mirroring the read-only spot-check parameters: -horizon-frac (2.0),
-min-samples (2), -wall-margin (1.0), and -floor-factor, which
defaults to 0 = walls-only operation (floors are the riskier half:
a too-high floor attracts flow and costs a failed part to
self-correct). Note the live floor default deliberately differs
from the spot-check commands' 0.9. Also dormant unless
clboss-rebalance-mode is xrebalance.
The per-cycle decision is a pure static XRebalancePredictor::plan
(group directions, candidacy gate, predict, collect asserting
sides, skip amount-0 degenerates), unit-tested directly; the module
shell only reads the table, executes the plan, logs one Info
summary per asserting cycle, and reports an xrebalance_predictor
section (params + last-cycle counts) in clboss-status.
kind_is_bound (stored TEXT kind to bound side) is promoted into
XRebalancePredict and shared with XRebalanceHistory.