Skip to content

fix(fusion): LearnableFusionPolicy must not train on an unranked union after re-enabling the tournament - #308

Merged
Birfy merged 1 commit into
mainfrom
fix/issue-302-learnable-fusion-ranked-inner
Oct 16, 2026
Merged

Birfy merged 1 commit into
mainfrom
fix/issue-302-learnable-fusion-ranked-inner

Conversation

@Birfy

@Birfy Birfy commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Closes #302

What

  • The default inner DefaultFusion is now always built with tournament=True; the outer LearnableFusionPolicy.tournament flag only selects the path (ranked vs. budget-degraded union path), so a False -> True flip (incl. the engine budget governor) reaches a real ranking.
  • _run_tournament forces ranking on a caller-supplied inner left at tournament=False for that call only (restored after), and refuses to count a run / train a label if the inner still recorded only unranked trials.
  • Budget-disabled union path (unranked trials, no labels) and the default behavior are unchanged.

Why: follow-up to the #298 review finding — after tournament=False -> True the inner returned an unranked union that was counted as a measured tournament and a positive label.

Tests (tests/test_learnable_fusion.py): False->True flip, True->False->True, epsilon=1 forced exploration, governor-style attribute flip, supplied unranked inner (restored after), inner that still returns unranked trials (no counters/labels). Instrumented verifier asserts real ranking calls; labels come from the measured winner. New tests fail on main; full pytest -q passes locally. Fragment: changelog.d/302-learnable-fusion-ranked-inner.md.

@Birfy
Birfy enabled auto-merge (squash) October 16, 2026 11:53
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T11:55:45.157658Z 8e71220 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Birfy
Birfy merged commit a5fde0e into main Oct 16, 2026
7 checks passed
@Birfy
Birfy deleted the fix/issue-302-learnable-fusion-ranked-inner branch October 16, 2026 12:10

@Birfy Birfy left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — #308 (Closes #302), head 8e71220

Verdict: LGTM ✅ (posted as a comment; self-approval is blocked, and auto-merge already squash-merged this at 20:10 CST, so the items below are follow-up nits, not blockers).

Correctness

  • Root cause is addressed in both places it could bite: the default inner is now always DefaultFusion(tournament=True), and _run_tournament forces ranking on a caller-supplied inner left at tournament=False, restoring it in a finally. The outer tournament flag now only chooses the path (ranked vs _union_policy), which matches the governor's fp.tournament = should contract in evolution.py / async_evolve.py.
  • I checked DefaultFusion.select: in tournament mode it always appends a ranked FusionTrial, and only _commit_union records ranked=False. So the new "no newly-recorded ranked trial → don't count, don't observe" guard never suppresses a legitimate label from the default inner. The existing default (tournament=True from construction) is byte-for-byte the same behaviour.
  • The budget-disabled path is untouched: no labels, and unranked trials still land on the shared trials list that fusion_stats reads.

Tests (run locally, Python 3.13, fresh clone)

  • tests/test_learnable_fusion.py passes on the branch. With agentdescent/fusion.py reverted to main, 5 of the 6 new tests fail, which confirms they reproduce #302. test_true_false_true_round_trip_ranks_on_the_second_enable passes on main too, because a True-built inner was never affected. That's fine as a regression guard, but "new tests fail on main" in the description is 5/6, not 6/6.
  • Full pytest -q on the branch: everything passed except tests/test_era_algotune.py::test_the_reference_scores_about_one_through_the_sandbox, which then passed in isolation on both the branch and main. It looks like a load-sensitive sandbox timing flake, not related to this PR. CI is green on build + 3.9–3.13.

Nits / follow-ups

  1. test_the_engine_governor_flip_to_on_ranks_through_evolve doesn't go through evolve. It reproduces the attribute write by hand, which is the same as the first test. #302 asked for the governor path specifically. A test that drives evolve (or async_evolve) with a budget that crosses the soft floor and then recovers would actually pin the integration. Otherwise, rename it so it doesn't overstate coverage.
  2. Temporarily mutating a caller-supplied inner (inner.tournament = True … finally: False) is fine for the engine, because the fusion decision runs at the round barrier and the async comment says no worker reads the flag. Still, it isn't reentrant if a user shares one DefaultFusion instance elsewhere. A cleaner long-term shape is a ranked entry point on the inner (e.g. a _rank(artifact, diffs) helper that select calls when tournament=True), so the label path never has to flip shared state.
  3. Unranked-only inners become silent no-op learners. With e.g. ReflectiveFusion as inner, which records ranked=False by contract and has no tournament attribute so it isn't forced, the wrapper now never counts a run or trains. That's correct per #302, but it would help to say so in the class docstring, or warn once in status(), so a user doesn't wonder why model_observations stays at 0.
  4. The trials[before:] slice relies on trials being append-only. It's documented that way on DefaultFusion, but a short comment here noting the dependency would help future readers.
  5. Docs: the tournament parameter's meaning changed slightly (it now picks the path, and no longer configures the inner). Consider one line in the class docstring / docs/api.md. The changelog fragment already describes it well.

Nice, tight fix: the scope stays within the acceptance criteria, and the fragment follows the new changelog.d convention.

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.

fusion: re-enabling LearnableFusionPolicy can train on an unranked union

1 participant