2026-07-24 13:01:06 -07:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Code-mode evaluator: the candidate is the full Go source of
|
|
|
|
|
cmd/routesim/candidate_impl.go — an entire routing algorithm.
|
|
|
|
|
|
|
|
|
|
Each eval compiles a routesim binary with the candidate swapped in via
|
|
|
|
|
`go build -overlay` (no working-tree mutation, parallel-safe) and runs it
|
|
|
|
|
with --router=candidate. Compile errors come back as feedback, which is
|
|
|
|
|
the highest-signal input a reflective proposer can get.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import subprocess
|
|
|
|
|
import tempfile
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import evaluate as params_eval
|
|
|
|
|
|
|
|
|
|
REPO = Path(os.environ.get(
|
|
|
|
|
"LND_REPO",
|
|
|
|
|
Path(__file__).resolve().parent.parent,
|
|
|
|
|
))
|
|
|
|
|
GO = os.environ.get("GO_BIN", "go")
|
|
|
|
|
|
|
|
|
|
# Tokens that have no business in a routing algorithm and defeat the
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
# information hiding of the simulator (reward-hack guard). The second
|
|
|
|
|
# group enforces the sealed-view invariant in the evaluator itself
|
|
|
|
|
# rather than by post-hoc grep: GraphSession callbacks receive the
|
|
|
|
|
# sealed view, and any candidate naming the hidden-state surfaces is
|
|
|
|
|
# probing for an escape.
|
2026-07-24 13:01:06 -07:00
|
|
|
BANNED = re.compile(
|
|
|
|
|
r'\b(unsafe|reflect|os/exec|syscall|net/http|io/ioutil)\b|'
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
r'"os"|_test\b|'
|
|
|
|
|
r'\b(LocalBalances|AssignLiquidity|BalanceNodeChannels|SendHtlc)\b|'
|
2026-07-25 17:40:23 -07:00
|
|
|
r'\b(HoldHtlc|SettleHold|ReleaseHold)\b|'
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
r'\*\s*routing\.SimGraph',
|
2026-07-24 13:01:06 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
FENCE = re.compile(r"^```(?:go)?\s*$|^```\s*$", re.MULTILINE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_source(candidate: str) -> str:
|
|
|
|
|
"""Strip markdown fences if the proposer wrapped the file in them."""
|
|
|
|
|
text = candidate.strip()
|
|
|
|
|
if text.startswith("```"):
|
|
|
|
|
text = FENCE.sub("", text).strip()
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
|
|
|
|
|
# Agentic proposers sometimes prepend prose despite instructions;
|
|
|
|
|
# a Go file must start at its package clause, so slice from there.
|
|
|
|
|
if not text.startswith("package "):
|
|
|
|
|
idx = text.find("package main")
|
|
|
|
|
if idx > 0:
|
|
|
|
|
text = text[idx:]
|
|
|
|
|
|
2026-07-24 13:01:06 -07:00
|
|
|
return text + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compile_candidate(source: str, workdir: Path) -> tuple[Path, str]:
|
|
|
|
|
"""Compile a routesim binary with the candidate overlaid. Returns
|
|
|
|
|
(binary path, "") on success or (None, compiler output) on failure."""
|
|
|
|
|
cand_path = workdir / "candidate_impl.go"
|
|
|
|
|
cand_path.write_text(source)
|
|
|
|
|
|
|
|
|
|
overlay = workdir / "overlay.json"
|
|
|
|
|
target = str(REPO / "cmd" / "routesim" / "candidate_impl.go")
|
|
|
|
|
overlay.write_text(json.dumps(
|
|
|
|
|
{"Replace": {target: str(cand_path)}},
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
binary = workdir / "routesim"
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
try:
|
|
|
|
|
proc = subprocess.run(
|
|
|
|
|
[GO, "build", "-overlay", str(overlay), "-o", str(binary),
|
|
|
|
|
"./cmd/routesim"],
|
|
|
|
|
cwd=REPO, capture_output=True, text=True, timeout=300,
|
|
|
|
|
)
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
# Under concurrent engines a slow build must score zero, not
|
|
|
|
|
# crash the whole run (raise_on_exception aborts on evaluator
|
|
|
|
|
# exceptions).
|
|
|
|
|
return None, "go build timed out after 300s (host under load?)"
|
2026-07-24 13:01:06 -07:00
|
|
|
if proc.returncode != 0:
|
|
|
|
|
return None, proc.stderr[-4000:]
|
|
|
|
|
|
|
|
|
|
return binary, ""
|
|
|
|
|
|
|
|
|
|
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
def run_compiled(binary, example) -> tuple[float, dict]:
|
|
|
|
|
"""Run a compiled candidate binary on one scenario file and score it."""
|
|
|
|
|
# A pathological candidate (infinite loop, quadratic blowup) must
|
|
|
|
|
# score 0, not crash the whole optimization run. 120s is generous:
|
|
|
|
|
# a healthy router does a full scenario batch in well under a
|
|
|
|
|
# second, so a timeout means the candidate is broken.
|
|
|
|
|
try:
|
|
|
|
|
proc = subprocess.run(
|
|
|
|
|
[str(binary), "--scenarios", str(example),
|
|
|
|
|
"--router", "candidate"],
|
|
|
|
|
capture_output=True, text=True, timeout=120,
|
|
|
|
|
)
|
|
|
|
|
except subprocess.TimeoutExpired:
|
2026-07-24 13:01:06 -07:00
|
|
|
return 0.0, {
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
"error": "timeout: candidate did not finish in 120s",
|
|
|
|
|
"hint": "The router likely loops without making progress "
|
|
|
|
|
"(e.g. RequestRoute never returns an error to terminate "
|
|
|
|
|
"the payment, or splits without shrinking). Ensure every "
|
|
|
|
|
"path terminates and shard amounts strictly decrease.",
|
|
|
|
|
}
|
|
|
|
|
if proc.returncode != 0:
|
|
|
|
|
return 0.0, {
|
|
|
|
|
"error": f"runtime failure: {proc.stderr[-2000:]}",
|
2026-07-24 13:01:06 -07:00
|
|
|
}
|
|
|
|
|
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
try:
|
|
|
|
|
output = json.loads(proc.stdout)
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
return 0.0, {
|
|
|
|
|
"error": "candidate produced no valid JSON output",
|
|
|
|
|
"stdout_tail": proc.stdout[-1000:],
|
|
|
|
|
}
|
2026-07-24 13:01:06 -07:00
|
|
|
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
try:
|
|
|
|
|
agg = output["aggregate"]
|
|
|
|
|
results = output["results"]
|
|
|
|
|
except (KeyError, TypeError):
|
|
|
|
|
return 0.0, {
|
|
|
|
|
"error": "routesim output missing aggregate/results",
|
|
|
|
|
}
|
2026-07-24 13:01:06 -07:00
|
|
|
|
simulation: name the fee metric, and the rule that guards its weight
In this commit, we move the objective's arithmetic into one function, put
the design rule that constrains its fee term next to the constants that
would break it, and give both evaluators the sentence about how to read a
falling fee.
The rule has a number in it. A scored file holds 6 to 10 payments, so
abandoning one payment in the smallest file costs 1/6 = 0.167 of
objective, while the entire fee term is worth at most FEE_PPM_CAP *
FEE_WEIGHT = 0.100. The fee term is therefore structurally incapable of
paying for abandonment, by a factor of 1.67, and that margin is the only
thing standing between it and the exp-013 give-up attractor. The rule:
the fee term's maximum value must stay strictly below 1/N, where N is the
payment count of the smallest scored file. Doubling the weight breaks it;
removing the cap breaks it unconditionally.
The safe way to make fees matter more is a different metric rather than a
bigger weight, so the metric is now named: FEE_METRIC is what every
published number was scored with, FEE_METRIC_ATTEMPTED is the alternative
whose denominator abandonment cannot shrink. composite_score takes either,
which is what lets the pre-registered arm re-score archived runs offline
with no re-execution and no change to what the optimizer maximizes.
The hint gains the fee rule in the unconditional style exp-017
established, because a thresholded warning fails here for the reason it
failed there: fees falling is not by itself evidence of anything. Fees
fall for two reasons, cheaper routes and fewer completed payments, and
only the first is an improvement. The code evaluator additionally tells a
candidate that a fee budget exists and where to read it, since a route
refused for cost spends an attempt and teaches nothing.
The scores are unchanged to the last bit, checked against the old inline
formula on the pre-change binary's own output.
2026-07-27 23:53:00 -07:00
|
|
|
fee_ppm = params_eval.capped_fee_ppm(agg)
|
|
|
|
|
score = params_eval.composite_score(agg)
|
2026-07-24 13:01:06 -07:00
|
|
|
|
simulation: adopt advisor corrections to measurement and validation
In this commit, we act on two independent advisor reviews that
reframed the program: the paradigm ceiling we have been attributing to
algorithm space is partly a measurement ceiling, and the validation
story has holes that would surface immediately upstream.
Measurement: the evaluator now emits separate objective axes
(success, retry efficiency with shards disentangled from retries, and
fee efficiency) so the engine's hybrid Pareto frontier can keep
specialists alive, and evaluation caching is enabled now that the
evaluator is verified deterministic. The split corpus generator gains
--split-leads, replacing the single ambitious payment -- which left
two thirds of every file's score as free probes and quantized
minibatch selection above the very signal being selected for -- with
a descending ladder of mandatory-split payments whose completion count
grades the score. The original --split output is regression-tested
byte-identical.
Validation: sweep_validate.py replaces ad-hoc sweeps with paired
per-file comparisons, bootstrap confidence intervals, and sign tests;
gen_mainnet_scenarios.py generates multi-vantage mainnet corpora with
log-spaced source degrees (2024 down to 2) so claims stop resting on a
single hub-resident vantage; and params_lnd_bimodal.json adds the
baseline arm reviewers will ask for first, since lnd ships a bimodal
estimator that our defaults-only comparisons never exercised. The
exp-010 writeup gains a pre-registered caveat, logged before the live
runs finish, that corpus resolution may mute their verdicts.
2026-07-25 02:58:52 -07:00
|
|
|
# Separate objective axes for Pareto-frontier preservation
|
|
|
|
|
# (frontier_type="hybrid" in the engine config). Retries and parts
|
|
|
|
|
# are disentangled: a mandatory 3-shard MPP payment is not "2 extra
|
|
|
|
|
# attempts" of waste, while 3 failures before 1 settle are. Axes are
|
|
|
|
|
# oriented so higher is better.
|
|
|
|
|
settled_parts = sum(
|
|
|
|
|
1
|
|
|
|
|
for res in results
|
|
|
|
|
for att in (res.get("attempts") or [])
|
|
|
|
|
if att.get("success")
|
|
|
|
|
)
|
|
|
|
|
total_attempts = agg["total_attempts"]
|
|
|
|
|
retries = max(total_attempts - settled_parts, 0)
|
|
|
|
|
num = max(agg["num_scenarios"], 1)
|
|
|
|
|
scores = {
|
|
|
|
|
"success": agg["success_rate"],
|
|
|
|
|
"retry_efficiency": -min(retries / num, 25.0),
|
|
|
|
|
"fee_efficiency": -fee_ppm / params_eval.FEE_PPM_CAP,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 00:07:29 -07:00
|
|
|
# Abandonment and background-settle rates are surfaced as top-level
|
|
|
|
|
# side information rather than left buried in the aggregate dict.
|
|
|
|
|
# exp-013 showed the composite objective hides giving up on hard
|
|
|
|
|
# payments inside the same number as attempt efficiency; a candidate
|
|
|
|
|
# at the attempt frontier can only "improve" by abandoning, and the
|
|
|
|
|
# reflection model needs to see that channel explicitly to avoid
|
|
|
|
|
# walking into it.
|
2026-07-27 00:21:54 -07:00
|
|
|
#
|
|
|
|
|
# A thresholded warning on give_up_rate alone does not work: exp-017
|
|
|
|
|
# measured that for candidate routers the field equals
|
|
|
|
|
# 1 - success_rate on every tier (a candidate "gives up" whenever it
|
|
|
|
|
# returns failure without exhausting the attempt budget, which is
|
|
|
|
|
# how candidates always fail), so such a warning fires universally
|
|
|
|
|
# and becomes noise. Abandonment is only readable JOINTLY, as low
|
|
|
|
|
# attempts together with low success, and the hint states that rule
|
|
|
|
|
# unconditionally instead.
|
2026-07-27 00:07:29 -07:00
|
|
|
give_up_rate = agg.get("give_up_rate", 0.0)
|
|
|
|
|
bg_settle_rate = agg.get("bg_settle_rate", 0.0)
|
|
|
|
|
|
|
|
|
|
hint = (
|
|
|
|
|
"success_rate dominates; attempts and fee ppm apply small "
|
|
|
|
|
"penalties. The router only sees gossip (no hidden "
|
|
|
|
|
"balances), its own channel balances, and per-attempt "
|
2026-07-27 00:21:54 -07:00
|
|
|
"failure feedback via ReportAttempt. Beware the give-up "
|
|
|
|
|
"attractor: a candidate can cut attempts by abandoning hard "
|
|
|
|
|
"payments, which the composite score partly rewards. Fewer "
|
|
|
|
|
"attempts is only an improvement if success_rate held or "
|
|
|
|
|
"rose; if attempts AND success both fell, the edit taught "
|
|
|
|
|
"the router to quit, not to route. (give_up_rate counts "
|
|
|
|
|
"payments failed without exhausting the attempt budget; for "
|
|
|
|
|
"most candidates it simply equals 1 - success_rate, so read "
|
|
|
|
|
"abandonment off the success/attempts pair, not off that "
|
simulation: name the fee metric, and the rule that guards its weight
In this commit, we move the objective's arithmetic into one function, put
the design rule that constrains its fee term next to the constants that
would break it, and give both evaluators the sentence about how to read a
falling fee.
The rule has a number in it. A scored file holds 6 to 10 payments, so
abandoning one payment in the smallest file costs 1/6 = 0.167 of
objective, while the entire fee term is worth at most FEE_PPM_CAP *
FEE_WEIGHT = 0.100. The fee term is therefore structurally incapable of
paying for abandonment, by a factor of 1.67, and that margin is the only
thing standing between it and the exp-013 give-up attractor. The rule:
the fee term's maximum value must stay strictly below 1/N, where N is the
payment count of the smallest scored file. Doubling the weight breaks it;
removing the cap breaks it unconditionally.
The safe way to make fees matter more is a different metric rather than a
bigger weight, so the metric is now named: FEE_METRIC is what every
published number was scored with, FEE_METRIC_ATTEMPTED is the alternative
whose denominator abandonment cannot shrink. composite_score takes either,
which is what lets the pre-registered arm re-score archived runs offline
with no re-execution and no change to what the optimizer maximizes.
The hint gains the fee rule in the unconditional style exp-017
established, because a thresholded warning fails here for the reason it
failed there: fees falling is not by itself evidence of anything. Fees
fall for two reasons, cheaper routes and fewer completed payments, and
only the first is an improvement. The code evaluator additionally tells a
candidate that a fee budget exists and where to read it, since a route
refused for cost spends an attempt and teaches nothing.
The scores are unchanged to the last bit, checked against the old inline
formula on the pre-change binary's own output.
2026-07-27 23:53:00 -07:00
|
|
|
"field alone.) " + params_eval.FEE_HINT + " A payment may also "
|
|
|
|
|
"carry a fee budget (spec.FeeLimitMsat, the total across all "
|
|
|
|
|
"shards, lnwire.MaxMilliSatoshi when there is none): a route "
|
|
|
|
|
"over budget is refused before it is sent, costing an attempt "
|
|
|
|
|
"and teaching nothing, and fee_limit_failures counts those "
|
|
|
|
|
"refusals."
|
2026-07-27 00:07:29 -07:00
|
|
|
)
|
|
|
|
|
|
2026-07-24 13:01:06 -07:00
|
|
|
return score, {
|
|
|
|
|
"score": score,
|
simulation: adopt advisor corrections to measurement and validation
In this commit, we act on two independent advisor reviews that
reframed the program: the paradigm ceiling we have been attributing to
algorithm space is partly a measurement ceiling, and the validation
story has holes that would surface immediately upstream.
Measurement: the evaluator now emits separate objective axes
(success, retry efficiency with shards disentangled from retries, and
fee efficiency) so the engine's hybrid Pareto frontier can keep
specialists alive, and evaluation caching is enabled now that the
evaluator is verified deterministic. The split corpus generator gains
--split-leads, replacing the single ambitious payment -- which left
two thirds of every file's score as free probes and quantized
minibatch selection above the very signal being selected for -- with
a descending ladder of mandatory-split payments whose completion count
grades the score. The original --split output is regression-tested
byte-identical.
Validation: sweep_validate.py replaces ad-hoc sweeps with paired
per-file comparisons, bootstrap confidence intervals, and sign tests;
gen_mainnet_scenarios.py generates multi-vantage mainnet corpora with
log-spaced source degrees (2024 down to 2) so claims stop resting on a
single hub-resident vantage; and params_lnd_bimodal.json adds the
baseline arm reviewers will ask for first, since lnd ships a bimodal
estimator that our defaults-only comparisons never exercised. The
exp-010 writeup gains a pre-registered caveat, logged before the live
runs finish, that corpus resolution may mute their verdicts.
2026-07-25 02:58:52 -07:00
|
|
|
"scores": scores,
|
2026-07-27 00:07:29 -07:00
|
|
|
"give_up_rate": give_up_rate,
|
|
|
|
|
"bg_settle_rate": bg_settle_rate,
|
2026-07-24 13:01:06 -07:00
|
|
|
"aggregate": agg,
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
"failed_payments": params_eval.summarize_failures(results),
|
2026-07-27 00:07:29 -07:00
|
|
|
"hint": hint,
|
2026-07-24 13:01:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
simulation: apply the library audit to the evaluator and runner
In this commit, we act on the deep audit of our optimize_anything
usage against the library source. The evaluator gains a batch form:
one Go compile per unique candidate instead of one per (candidate,
example) pair, which turns an eight-file valset pass from eight
identical builds into one -- a several-fold wall-clock win the gepa
engine consumes through the batch_evaluator hook, now wired into both
runner paths. Crash paths that could abort an overnight run are
closed: a build timeout under host load and malformed routesim output
now score zero with feedback instead of raising through the engine,
and raise_on_exception is off as a backstop, with
max_candidate_proposals as the enforceable cap now that evaluation
caching makes max_evals count only misses.
The reward-hack guard moves from post-hoc greps into the evaluator
itself: the banned-token regex now covers the hidden-state surfaces
(LocalBalances, AssignLiquidity, BalanceNodeChannels, SendHtlc, and
*routing.SimGraph assertions), verified clean against the seed and
every archived candidate. Fence stripping also slices from the
package clause when an agentic proposer prepends prose. Finally, the
codex harness home moves reflection to high reasoning effort --
medium was an unexamined default for 300-800 line Go rewrites.
2026-07-25 03:05:41 -07:00
|
|
|
def evaluate(candidate: str, example) -> tuple[float, dict]:
|
|
|
|
|
"""The optimize_anything evaluator contract for code candidates."""
|
|
|
|
|
source = extract_source(candidate)
|
|
|
|
|
|
|
|
|
|
banned = BANNED.search(source)
|
|
|
|
|
if banned:
|
|
|
|
|
return 0.0, {
|
|
|
|
|
"error": f"banned identifier {banned.group(0)!r}: candidates "
|
|
|
|
|
"must not use unsafe/reflect/os/exec/net or probe the hidden "
|
|
|
|
|
"simulator state — pure routing logic only.",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="routesim-cand-") as tmp:
|
|
|
|
|
workdir = Path(tmp)
|
|
|
|
|
|
|
|
|
|
binary, compile_err = compile_candidate(source, workdir)
|
|
|
|
|
if binary is None:
|
|
|
|
|
return 0.0, {
|
|
|
|
|
"error": "compile failed",
|
|
|
|
|
"compiler_output": compile_err,
|
|
|
|
|
"hint": "Return the COMPLETE contents of "
|
|
|
|
|
"candidate_impl.go (package main), defining "
|
|
|
|
|
"newCandidateRouter with the exact contract signature.",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return run_compiled(binary, example)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def batch_evaluate(pairs) -> list:
|
|
|
|
|
"""Batch form of the evaluator contract: one compile per unique
|
|
|
|
|
candidate instead of one per (candidate, example) pair.
|
|
|
|
|
|
|
|
|
|
A candidate's valset pass previously ran `go build` once per
|
|
|
|
|
example — eight identical compiles for an eight-file valset. Here
|
|
|
|
|
the pairs are grouped by extracted source, each unique candidate is
|
|
|
|
|
compiled once, and its scenario runs execute against the shared
|
|
|
|
|
binary. Returns results in input order.
|
|
|
|
|
"""
|
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
|
|
|
|
|
|
order = list(pairs)
|
|
|
|
|
by_source: dict = {}
|
|
|
|
|
for idx, (candidate, example) in enumerate(order):
|
|
|
|
|
by_source.setdefault(extract_source(candidate), []).append(
|
|
|
|
|
(idx, candidate, example),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
results: list = [None] * len(order)
|
|
|
|
|
|
|
|
|
|
def run_group(group) -> None:
|
|
|
|
|
source, members = group
|
|
|
|
|
banned = BANNED.search(source)
|
|
|
|
|
if banned:
|
|
|
|
|
outcome = (0.0, {
|
|
|
|
|
"error": f"banned identifier {banned.group(0)!r}: "
|
|
|
|
|
"candidates must not use unsafe/reflect/os/exec/net or "
|
|
|
|
|
"probe the hidden simulator state — pure routing logic "
|
|
|
|
|
"only.",
|
|
|
|
|
})
|
|
|
|
|
for idx, _, _ in members:
|
|
|
|
|
results[idx] = outcome
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="routesim-cand-") as tmp:
|
|
|
|
|
workdir = Path(tmp)
|
|
|
|
|
binary, compile_err = compile_candidate(source, workdir)
|
|
|
|
|
if binary is None:
|
|
|
|
|
outcome = (0.0, {
|
|
|
|
|
"error": "compile failed",
|
|
|
|
|
"compiler_output": compile_err,
|
|
|
|
|
"hint": "Return the COMPLETE contents of "
|
|
|
|
|
"candidate_impl.go (package main), defining "
|
|
|
|
|
"newCandidateRouter with the exact contract "
|
|
|
|
|
"signature.",
|
|
|
|
|
})
|
|
|
|
|
for idx, _, _ in members:
|
|
|
|
|
results[idx] = outcome
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for idx, _, example in members:
|
|
|
|
|
results[idx] = run_compiled(binary, example)
|
|
|
|
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
|
|
|
list(pool.map(run_group, by_source.items()))
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 13:01:06 -07:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
seed_path = REPO / "cmd" / "routesim" / "candidate_impl.go"
|
|
|
|
|
score, info = evaluate(seed_path.read_text(), sys.argv[1])
|
|
|
|
|
print(f"score={score:.4f}")
|
|
|
|
|
print(json.dumps(info.get("aggregate", info), indent=2))
|