MoA: don't let small-model consensus pre-empt a still-running large model - #837
Conversation
…indow When the mesh mixes a big-tier model (e.g. MiniMax) with small-tier workers, two fast small models agreeing could finalize a mesh turn before the strong worker produced anything — dumbing the answer down to small-model consensus. Research (Self-MoA, arXiv:2502.00674) shows MoA quality tracks proposer quality far more than diversity. This adds a tier gate to the fan-out decision loop: - Small-tier-only answer consensus, small-tier sole-survivor answers, and the answer grace timer are held while the big-tier Strong worker is still running — bounded by a new strong_patience window (20s default at the MoA gateway). - The hold is a hard bound: at expiry every decision rule reverts to pre-gate behavior, so a stuck strong worker can never hold the turn hostage (the failure mode that sank PR #820). A dedicated wake-up in the select loop re-evaluates held outputs at expiry rather than waiting for worker_timeout. - Consensus that includes the strong worker's answer ships immediately (agreement WITH the strong model, not against it). - Tool proposals are exempt: they are schema-verified by tool_guard and agent loops (goose/claw) must stay snappy. - Same-tier pools (many small models lifting each other) are detected via has_quality_gap and keep the existing latency profile untouched. - Answer grace now prefers the Strong worker's qualifying answer over marginally-higher self-reported confidence from smaller models. Timing knobs are grouped into a GatherPolicy struct. New sim tests pin the held-consensus, patience-expiry, and same-tier contracts.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThis PR introduces a configurable "strong patience" timeout that gates early arbitration decisions in mixed-tier worker pools, preventing small-tier consensus from finalizing while a Strong worker is still active within the patience window, with hard expiry releasing held consensus. ChangesStrong-Worker Patience Tier Gating
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs (1)
161-165: ⚡ Quick winTighten the latency bound to the configured patience window.
elapsed < 5swill still pass if the release regresses from~500msto several seconds, so it does not really pin the “promptly at patience expiry” contract described above. A much tighter upper bound with some scheduler slack would catch that regression.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs` around lines 161 - 165, The assertion currently allows up to 5s (assert!(elapsed < Duration::from_secs(5), ...)) which is too loose; replace that upper bound with a tighter timeout near the configured patience window (~500ms) plus scheduler slack (e.g., ~750ms or 1s) so regressions are caught—update the assertion comparing elapsed to a Duration reflecting that tightened bound and keep the existing error message referencing elapsed.crates/mesh-mixture-of-agents/src/fanout.rs (1)
257-264: 💤 Low valuePanicked/cancelled Strong worker leaves
strong_finishedfalse, gate holds until patience expires.When a task produces a
JoinError, we have no(model, role)payload to identify the worker. If the Strong worker panics,strong_finishedstaysfalseand the gate continues holding until the patience window expires rather than releasing immediately.This is a minor edge case (panicking workers are rare), and patience expiry is the bounded fallback. No action required unless this becomes a real latency issue.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/mesh-mixture-of-agents/src/fanout.rs` around lines 257 - 264, The JoinError handling block in fanout.rs currently increments total_finished and logs the error but does not mark the Strong worker as finished, so a panicked/cancelled Strong leaves strong_finished false and the gate waits until patience expires; update the Err(e) arm that logs "moa: worker task panicked or was cancelled" to also detect if the missing worker was the Strong worker (by correlating the JoinError to the dispatched slot or checking the worker identity used when spawning) and set strong_finished = true (or otherwise mark that slot as completed) so the gate can release immediately; ensure this change integrates with reconcile_dispatched logic that expects dispatched summaries by name.crates/mesh-mixture-of-agents/src/arbiter.rs (1)
347-380: 💤 Low valueVariable
strong_agreesis misleading — it checks whether Strong has finished with any answer, not cluster agreement.The docstring says "Consensus that includes the strong worker's answer passes through" but
strong_agreesonly checks if Strong produced any usable answer, not whether Strong's answer is part of the agreeing cluster. If Strong answered "Berlin" while two small models agreed on "Paris",strong_agreeswould betrueand the "Paris" consensus would ship.If this is intentional (Strong finishing lifts the gate regardless of agreement), consider renaming to
strong_has_answeredand adjusting the docstring to clarify that Strong's mere presence (not agreement) releases the hold.Suggested clarification
- let strong_agrees = answers + // Strong has produced a usable answer — it had its chance to weigh in. + // Whether Strong agrees with the cluster or not, the gate releases. + let strong_has_answered = answers .iter() - .any(|a| a.role == WorkerRole::Strong && is_usable_answer(a)); - if strong_pending && !strong_agrees { + .any(|a| a.role == WorkerRole::Strong); + if strong_pending && !strong_has_answered {Note:
is_usable_answer(a)is redundant sinceanswersis already filtered by that predicate.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/mesh-mixture-of-agents/src/arbiter.rs` around lines 347 - 380, In tier_aware_consensus_decision change the misleading strong_agrees semantics: either (A) if intent is "strong finishing lifts the gate regardless of content" rename strong_agrees to strong_has_answered and update the function/doc comment to state that any usable Strong answer releases the hold (also remove redundant is_usable_answer check since answers is already filtered), or (B) if intent is "strong must agree with the cluster" change the check to compare Strong's payload to the agreeing cluster (i.e., find the Strong WorkerOutput in answers and verify its payload matches the best consensus payload) before treating the gate as lifted; update variable names (e.g., strong_matches_consensus) and the docstring accordingly to reflect the chosen behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs`:
- Around line 118-145: The test only asserts that the strong worker (model
"MiniMax-M2.5") completed but does not assert that its answer was selected;
update the test to assert the turn response equals the strong worker's answer by
calling response_text(&result) and comparing it to the expected strong-worker
text (the answer produced by the strong model in this scenario), ensuring the
contract that "when the strong worker lands with a usable answer, it wins" is
enforced; locate the test function small_consensus_is_held_until_strong_lands,
the result variable from moa::handle_turn(&config, &user_turn(...)), and add an
assertion that response_text(&result) == "<expected strong answer>" (or match
the known strong summary output) after verifying strong_summary.succeeded.
---
Nitpick comments:
In `@crates/mesh-mixture-of-agents/src/arbiter.rs`:
- Around line 347-380: In tier_aware_consensus_decision change the misleading
strong_agrees semantics: either (A) if intent is "strong finishing lifts the
gate regardless of content" rename strong_agrees to strong_has_answered and
update the function/doc comment to state that any usable Strong answer releases
the hold (also remove redundant is_usable_answer check since answers is already
filtered), or (B) if intent is "strong must agree with the cluster" change the
check to compare Strong's payload to the agreeing cluster (i.e., find the Strong
WorkerOutput in answers and verify its payload matches the best consensus
payload) before treating the gate as lifted; update variable names (e.g.,
strong_matches_consensus) and the docstring accordingly to reflect the chosen
behavior.
In `@crates/mesh-mixture-of-agents/src/fanout.rs`:
- Around line 257-264: The JoinError handling block in fanout.rs currently
increments total_finished and logs the error but does not mark the Strong worker
as finished, so a panicked/cancelled Strong leaves strong_finished false and the
gate waits until patience expires; update the Err(e) arm that logs "moa: worker
task panicked or was cancelled" to also detect if the missing worker was the
Strong worker (by correlating the JoinError to the dispatched slot or checking
the worker identity used when spawning) and set strong_finished = true (or
otherwise mark that slot as completed) so the gate can release immediately;
ensure this change integrates with reconcile_dispatched logic that expects
dispatched summaries by name.
In `@crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs`:
- Around line 161-165: The assertion currently allows up to 5s (assert!(elapsed
< Duration::from_secs(5), ...)) which is too loose; replace that upper bound
with a tighter timeout near the configured patience window (~500ms) plus
scheduler slack (e.g., ~750ms or 1s) so regressions are caught—update the
assertion comparing elapsed to a Duration reflecting that tightened bound and
keep the existing error message referencing elapsed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 076da41c-6532-4c86-8b8f-e0030df34e90
📒 Files selected for processing (11)
crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rscrates/mesh-mixture-of-agents/src/arbiter.rscrates/mesh-mixture-of-agents/src/fanout.rscrates/mesh-mixture-of-agents/src/lib.rscrates/mesh-mixture-of-agents/src/worker.rscrates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rscrates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rscrates/mesh-mixture-of-agents/tests/sim_strong_patience.rscrates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rscrates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rscrates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
When the held strong worker lands but disagrees with the small-tier consensus, ship the strong worker's answer rather than small-model consensus. Holding for the strong worker only bought it a seat; this makes its answer actually win on disagreement, which is the point of the tier gate (don't let small models outvote the big one). Review feedback addressed: - sim_strong_patience: assert the strong answer actually wins, not just that the strong worker finished (CodeRabbit major). - sim_strong_patience: tighten patience-expiry latency bound from 5s to 1.5s so several-second regressions toward worker_timeout are caught. - fanout: document that a panicked Strong worker intentionally falls back to bounded patience expiry rather than adding fragile JoinError->slot correlation. - Add arbiter unit test pinning strong-dissent-wins behavior.
* origin/main: Add transport-aware Skippy stage ordering (#814) Share Skippy stage wire byte accounting (#818) Report Skippy artifact cold-start costs (#815) fix: debug output capturing for TUI / panics (#827) fix(hero): visual corrections for iPhone SE size devices (#838) Add Skippy stage role metadata (#816) Add Skippy request cache epoch telemetry (#817) Consolidate agent skills and fix stale docs (Windows deploy, repo map, design docs) (#836) feature(version): normalize version markers for different build types (#831) fix(website): fix visual regressions (#835) fix(gh): change micn to michaelneale in auto_assign.yml Revert "fix(gh): replace micn with IvGolovach in auto_assign.yml (not a collaborator)" fix(gh): replace micn with IvGolovach in auto_assign.yml (not a collaborator)
* origin/main: (29 commits) MoA: don't let small-model consensus pre-empt a still-running large model (#837) fix(console): render thinking traces as markdown Add bounded direct path repair (#846) Fix skippy smoke PR gate (#850) Stabilize skippy smoke chain startup (#849) fix(ci): switch back to auto-assign workflow fix(website): polish longform visual explainer (#843) fix: gemma thinking Carry GLM llama MTP patches (#840) Refresh llama.cpp canary patch queue (#839) Add transport-aware Skippy stage ordering (#814) Share Skippy stage wire byte accounting (#818) Report Skippy artifact cold-start costs (#815) fix: debug output capturing for TUI / panics (#827) fix(hero): visual corrections for iPhone SE size devices (#838) Add Skippy stage role metadata (#816) Add Skippy request cache epoch telemetry (#817) Consolidate agent skills and fix stale docs (Windows deploy, repo map, design docs) (#836) feature(version): normalize version markers for different build types (#831) fix(website): fix visual regressions (#835) ... # Conflicts: # AGENTS.md
…odel (#837) * MoA: hold small-tier consensus for a bounded strong-worker patience window When the mesh mixes a big-tier model (e.g. MiniMax) with small-tier workers, two fast small models agreeing could finalize a mesh turn before the strong worker produced anything — dumbing the answer down to small-model consensus. Research (Self-MoA, arXiv:2502.00674) shows MoA quality tracks proposer quality far more than diversity. This adds a tier gate to the fan-out decision loop: - Small-tier-only answer consensus, small-tier sole-survivor answers, and the answer grace timer are held while the big-tier Strong worker is still running — bounded by a new strong_patience window (20s default at the MoA gateway). - The hold is a hard bound: at expiry every decision rule reverts to pre-gate behavior, so a stuck strong worker can never hold the turn hostage (the failure mode that sank PR #820). A dedicated wake-up in the select loop re-evaluates held outputs at expiry rather than waiting for worker_timeout. - Consensus that includes the strong worker's answer ships immediately (agreement WITH the strong model, not against it). - Tool proposals are exempt: they are schema-verified by tool_guard and agent loops (goose/claw) must stay snappy. - Same-tier pools (many small models lifting each other) are detected via has_quality_gap and keep the existing latency profile untouched. - Answer grace now prefers the Strong worker's qualifying answer over marginally-higher self-reported confidence from smaller models. Timing knobs are grouped into a GatherPolicy struct. New sim tests pin the held-consensus, patience-expiry, and same-tier contracts. * MoA: prefer strong worker's answer on dissent; address review feedback When the held strong worker lands but disagrees with the small-tier consensus, ship the strong worker's answer rather than small-model consensus. Holding for the strong worker only bought it a seat; this makes its answer actually win on disagreement, which is the point of the tier gate (don't let small models outvote the big one). Review feedback addressed: - sim_strong_patience: assert the strong answer actually wins, not just that the strong worker finished (CodeRabbit major). - sim_strong_patience: tighten patience-expiry latency bound from 5s to 1.5s so several-second regressions toward worker_timeout are caught. - fanout: document that a panicked Strong worker intentionally falls back to bounded patience expiry rather than adding fragile JoinError->slot correlation. - Add arbiter unit test pinning strong-dissent-wins behavior.
Summary
When
model: "mesh"fans out across a mixed mesh — one large model (e.g. MiniMax on studio) plus several small models — the large model's answer could be silently discarded: two fast small workers agreeing (or the 3s answer-grace timer) finalized the turn while the large model was still prefilling. Users got small-model consensus even when a much stronger answer was seconds away.After this change, MoA holds small-tier-only answers for a bounded patience window (20s) when a big-tier Strong worker is still running:
worker_timeout.tool_guard, and agent loops (goose / OpenClaw) keep their current latency.has_quality_gaponly arms the gate when the Strong worker is big-tier and small-tier workers are present. Many-small-models meshes keep today's early-exit latency, preserving the consensus-lift behavior.Why
Self-MoA (arXiv:2502.00674) shows MoA output quality is far more sensitive to proposer quality than diversity — mixing weak proposers into a strong model's pool actively hurts. "Are More LLM Calls All You Need?" (arXiv:2403.02419) shows majority voting degrades on hard queries. The goal of mesh MoA is to lift intelligence when there are many comparable small models, not to dumb down a large one; this change separates those two regimes.
Architecture
worker::has_quality_gap— tier analysis over(model_name, role)pairs, reusing the existing single-digit-B name heuristic shared with the router.arbiter::StrongGate— explicit gate state passed intotry_early_decision;Offreproduces pre-change behavior exactly (all existing arbiter tests pass unchanged withOff).fanout::GatherPolicy— groups the timing knobs (first_answer_grace,grace_mode, newstrong_patience).GatewayConfig.strong_patience— zero disables the gate entirely; sim tests run with zero except the new gate-specific sims.Protocol
No wire/protocol changes. Purely host-local decision logic inside the MoA gateway; mixed-version meshes unaffected.
Validation
cargo test -p mesh-mixture-of-agents— 158 lib tests + all sims pass, including newsim_strong_patience.rspinning: held consensus until strong lands, hard release at patience expiry with a hung strong worker (anti-Stabilize mesh MoA context and tool loops #820), and same-tier pools keeping early-exit.cargo test -p mesh-llm-host-runtime --lib— 1439 passed.-D warningsclean on both crates; fmt clean.Summary by CodeRabbit
New Features
Refactor
Tests