The previous commit fed only the FAILURE side: probes that hit
WIRE_TEMPORARY_CHANNEL_FAILURE etc. wrote constraints into the
clboss layer. Successful probes (id1 returned
WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, the expected
outcome) returned bool=true and did not touch the layer.
This commit adds the symmetric positive feed: when a probe
succeeds, call askrene-inform-channel inform=succeeded on
chan1 (peer's outgoing channel to id1) at amount1. askrene
treats this as a raised lower-bound on the directed channel's
capacity estimate, so future getroutes calls assign chan1 a
higher success-probability score.
Why this is the dominant signal source in practice:
Failures are stochastic and bounded (one channel-or-node can
only fail a few times before askrene routes around it
entirely). Successes are repeated -- the same chan1 can
push the same amount1 many times, raising the lower bound
monotonically. Over a day of probes, the success-feed
contribution to the layer's expected-flow estimate
vastly exceeds the failure-feed contribution.
Symmetric with the failure path on which channels we feed:
Only chan1 (peer's outbound to id1) is fed. chan0 (our
outbound to peer) is deliberately skipped on the success
path for the same reason it is skipped on the failure path:
CLBOSS already has authoritative knowledge of local channel
state via listpeerchannels, and the shared askrene layer
exists for cross-subsystem knowledge that is not already
authoritatively available locally.
No new RPC types, no new error paths. Boss::Mod::AskreneLayer::
inform_channel_succeeded was already added in the earlier
shared-helpers refactor commit anticipating exactly this use.
When a 2-hop probe sendpay fails, parse the waitsendpay error and
record real route failures into the persistent clboss askrene
layer via Boss::Mod::AskreneLayer's shared helpers. Future
getroutes calls (FundsMover's rebalances, ActiveProber's own
subsequent probes, anything else that consumes the clboss layer)
automatically steer around the failed channel or node.
The PR2 commit introduced the layer + the inform-channel /
disable-node call shape, but FundsMover is the only writer
today, and its writes are gated behind real sendpay attempts
that rarely fire (fees on small rebalance amounts almost always
exceed the prorated budget, so FundsMover gives up
pre-sendpay). Net result: the layer is essentially empty in
production.
ActiveProber, by contrast, runs on a timer and sends real
sendpays without a budget filter. Wiring its failure path to
the layer turns it into the dominant feeder, making the layer
infrastructure observable and useful.
Probe-outcome discrimination -- the failure interpretation is
necessarily tighter than FundsMover's because every probe is
designed to fail at the final hop:
failcode 0x400F (WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS)
-> id1 received the HTLC and rejected it
because our bit-flipped random
payment_hash matches no invoice. This is
the EXPECTED outcome: the route was
viable all the way to id1. Probe
success; no layer write.
code 202 (unparsable onion)
-> cannot pin the failure to a specific
channel or node. On a 2-hop route, id1
is the most likely culprit; disable_node(id1).
echan == chan0 -> our outgoing channel had the issue. Skip
the layer write: CLBOSS already has
authoritative knowledge of local channel
state via listpeerchannels, and the
shared layer is for inter-subsystem
knowledge that isn't already authoritatively
available.
failcode & 0x2000 (NODE-level)
-> disable_node(erring_node).
channel-level failcode on chan1
-> inform_channel_constrained(chan1, dir1, amount1).
unparseable error data
-> swallow; don't guess.
No change to the existing probe-success accounting: the bool
returned by the catching handler still flows into the same
delpay-status / "Finished probing peer X" logic. The only new
behavior is the side-effect layer write inside record_failure.
Open question, deliberately not addressed here: should
ActiveProber ALSO feed inform=succeeded on probe success
(raising lower-bound estimates on chan1)? The DEVSTATE
ACTIVE-PROBER-LAYER-FEEDER-2026-05-20 recommends yes, ~5 lines
extra; defer until empirical signet observation shows whether
the failure-feedback alone is enough to drive observable
routing improvements.
Continuation of the v26.06 migration started in the Dowser
commit. Two more getroute call sites:
ChannelCandidateMatchmaker.cpp and ActiveProber.cpp. Both use
maxparts=1 since each wants a single route, not a flow estimate.
ActiveProber probes the local node's own outbound liquidity, so
it uses layers=["auto.localchans","auto.sourcefree"] per the
standardized recipe. Matchmaker probes from a remote source
(a candidate patron) to a remote target, so it passes an empty
layers array -- auto.localchans would inject our private local
channels into a foreign source, and auto.sourcefree would zero
out the source's outgoing fees, either of which could make
askrene pick a patron the proposal cannot actually reach via
public topology. maxfee_msat is 1% of the probe amount in both
cases.
Matchmaker is a near-clean swap. The patron id (route[0].id in
the old getroute shape) is read from routes[0].path[0] in the
new getroutes shape; at the parse site we bridge the v26.06+
name (node_id_out) with its deprecated pre-v26.06 predecessor
(next_node_id) via has()/ternary, so the code works on either
CLN flavor.
ActiveProber is more involved. The previous code stashed
res["route"] as a Jsmn::Object and later appended hops from it
directly into the sendpay route parameter, relying on the fact
that the old getroute hop shape (id/channel/direction/
amount_msat/delay/style) was already sendpay-compatible. The
new getroutes path[] shape is NOT directly sendpay-compatible
(different field names, short_channel_id_dir encodes scid and
direction together), so we now extract path[0] into typed
values (id1, chan1, direction1, amount1, delay1) and rebuild
the sendpay hop1 explicitly from those. The Jsmn::Object route
member is dropped. The hop-field reads in ActiveProber bridge
between v26.06+ (node_id_out / amount_out_msat / cltv_out) and
the deprecated pre-v26.06 names (next_node_id / amount_msat /
delay) via the same has()/ternary pattern.
short_channel_id_dir splits on '/' with an explicit npos check;
a missing slash throws Jsmn::TypeError so the surrounding parse-
error log path catches it cleanly. Without the check,
sdir.substr(0, npos) and sdir.substr(npos + 1) would feed
malformed input into Ln::Scid (which throws
std::invalid_argument, not caught by the Jsmn::TypeError
handler) or into std::stoul (silently producing a wrong
direction).
ActiveProber also drops its vestigial exclude=[self_id]
parameter: askrene's source/destination model naturally excludes
self when source != self, which is always the case here (the
probe always flows from peer outward, never back through us).
Three of the four getroute call sites in CLBOSS are now on
getroutes; the fourth (FundsMover/Attempter) is deferred to PR2
because its exclude-vector pattern encodes failure feedback from
real payment attempts and warrants a redesign around
askrene-inform-channel rather than a mechanical port.