Skip to content

MoA: mesh answers improve as capable nodes join - #1116

Merged
michaelneale merged 77 commits into
mainfrom
feat/moa-synthesis
Aug 6, 2026
Merged

MoA: mesh answers improve as capable nodes join#1116
michaelneale merged 77 commits into
mainfrom
feat/moa-synthesis

Conversation

@michaelneale

@michaelneale michaelneale commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Ready for review. Every number below is measured through the shipped entrypoint (moa::handle_turn) against real open-weight models; method, raw numbers, and withdrawn results are in evals/moa-openrouter/RESULTS.md.

Closes part of #1115.

What this delivers

The virtual mesh model gets stronger as capable nodes join, degrades gracefully when there aren't any, and never convenes a committee that measured worse than serving one model.

  • Reasoning/answer turns → workers draft in parallel, one aggregator synthesizes. Always synthesizes on a multi-worker answer turn — shipping one worker's text verbatim was a pure loss mode.
  • Tool turns → route to the single best tool-caller (voting/advice measured null-to-harmful there).
  • Everything best-effort → bounded waits; a slow or dead peer costs the grace window (10s), not the worker timeout.

The core result

Capable pool (qwen3-32b, mistral-small-24b, qwen3.5-35b-a3b, qwen3-14b), vs the pool's best single member answering alone. 40 preregistered prompts × 2 draws, out-of-family position-swapped judge, length logged:

71W / 8T / 1L, p<0.0001 — and it wins 28 of 29 trials where the MoA answer was shorter than solo, so this is not a length artifact. Reproduced across three runs (65W/0L, 62W/1L, 71W/1L).

Before the fixes below, the same pool through the same entrypoint lost 26W/14T/40L.

Three shipped-path bugs

The mechanism was fine; the wiring never ran it.

  1. Grace pre-empted synthesis. first_answer_grace armed on the first answer and finalized the turn, so on fast backends it fired on ~every turn and shipped one worker's text — measured 80/80 early-exit at capable scale. Grace now finalizes on tool turns only; on answer turns it is a collection deadline (stop waiting for the tail, then synthesize what arrived). Private-mesh window 3s → 10s.
  2. Role-tiered budgets truncated the drafts synthesis consumes (Fast 256 / Specialist 512). Those tiers existed to make the grace fast-path cheap; answer turns now pack every worker at the full budget.
  3. Answer turns shipped one worker verbatim when drafts agreed. Agreeing drafts are the best input to synthesis, not a reason to skip it.

Pool shaping (all measured)

  • Verified sizing — tiers come from GGUF tensor sums gossiped via the existing parameter_count_b, not name parsing. Name parsing mis-tiered real models (gemma-4-E4B stores 7.5B, not 4B). No verified size ⇒ ranks small, so an unparseable alias can't pose as big-tier. (i386 P1s.)
  • Admission — verified-small workers are dropped only when ≥2 verified-big remain. A lone big + smalls keeps the mix, because dropping the smalls collapses the committee to a solo model (that mix measured 47W/27T/5L vs solo).
  • All-small pools serve their best member. An 8B-class pool with an 8B reducer never beat its best member: 2×8B 0W/43T/37L, 4×8B 5W/63T/12L, 6×8B 5W/52T/23L (p=0.0009), with consistently shorter answers. Rather than ship a measured regression, such a pool collapses to its strongest member and the request degrades to serving it. Scope: this is a weak reducer synthesizing weak drafts, not evidence small-model MoA cannot work — the untested cell is small peers with a strong reducer. One mid-size node changes the pool from all-small, and MoA engages again.
  • Committee cap — 6 all-small, 4 once a verified big is present. Fan-out is ~2N+1 calls/turn and quality is flat past ~4, so a 20-node mesh doesn't fan out to 20.
  • Graceful degrade — a single model (or a collapsed pool) rewrites mesh to a real served model and routes normally. Only a node serving nothing returns 503. On main this is a 503.

Withdrawn

The width-sprint headline — "6× diverse 8B beats its best member, 12W/65T/2L, p=0.013" — did not replicate. The same rig on the same 40 tasks and pool now gives 3W/76T/1L (p=0.63), with 11 of 40 tasks flipping verdict. It rested on ~14 decided trials out of 80 (the rest ties) from a single unreplicated run selected out of a width sweep. Withdrawn in RESULTS.md and it motivated the all-small gate above.

Also fixed

Nine eval-vs-production divergences found by measuring the shipped path (grace finalizing before synthesis; reducer/refiner truncation discarding most of each answer; prompts telling workers to "be concise"/"be direct"; named vs anonymous reducer inputs; the worker preamble on the reducer; role-tier token starvation). Plus a production panic in mesh-llm-guardrails (byte-slicing a multi-byte character), with regression tests. Two hypotheses were tested, rejected by measurement, and reverted rather than rationalised — including one of my own (min_grace_answers = N-1) that stalled public-mesh turns to the 60s worker timeout.

Protocol / compatibility

No wire, gossip, plugin, or skippy-ABI changes. Sizing reads the existing ServedModelMetadata.parameter_count_b (now populated from GGUF tensor sums instead of a name regex). New behaviour is in-memory config (ReferencePolicy, RefinementPolicy). One caller-visible change: reasoning_effort/enable_thinking on a model=mesh request is ignored — that override only selected a broken config where reasoning models return content: null.

Validation

cargo test -p mesh-mixture-of-agents        all pass
cargo test -p mesh-llm-host-runtime        1911 pass
cargo test -p mesh-llm-guardrails            19 pass
clippy -D warnings (moa, host-runtime, guardrails, mesh-llm)   0 errors
cargo fmt --all --check                                        clean

Robustness contracts verified by simulation: a slow worker cannot stall a turn, a lone survivor still answers, a hanging refiner cannot hold the turn, patience expiry releases held consensus, partial worker death is fully accounted.

Live release check on this branch (built here, real node): local private inference, model=mesh on a single node (200, degrades), console index + assets + /api/status + /api/models + /v1 passthrough chat + SSE streaming, joined the public mesh (13–16 peers, mixed 0.65.1/0.72.1/0.74.0, 12-model union), MoA fanout there with 2 of 6 workers failing and the turn still completing, and model-by-name routing to a remote peer.

Public-mesh contrast vs released v0.74.0 (same 3B model, same prompts, model=mesh): released never synthesizes (reducer=false on all three probes, two early-exiting at 3–4s); this branch always synthesizes. An earlier revision of this branch rode the 60s worker timeout on two of three probes — root-caused to min_grace_answers, reverted, and the grace window now bounds a dead peer at 10s.

Honest limits (for the reviewer)

  • Quality numbers come from the OpenRouter-backed harness driving the real handle_turn, not a live capable-node mesh. The mesh-config paths (admission, cap, all-small gate, self-fill) are unit-tested and the plumbing is live-verified, but the capable-pool quality lift itself has not been reproduced on real mesh hardware.
  • Single judge model. A judge-level length bias was found and controlled (r fell from +0.68 to +0.13–0.30); the capable-pool result survives it in the hard direction (wins while shorter), the small-pool results are marginal either way.
  • judge_pair collapses genuine ties, position disagreements, and API/parse failures into the same 0, so tie counts cannot be read as agreement.
  • Significance is computed per draw (80) rather than per prompt cluster (40), which overstates confidence. The capable-pool result holds either way; the small-pool ones are marginal.
  • Ties dominate at small scale — "stronger" means wins-a-minority and rarely-loses, not uniformly better.
  • The +14.8k line count is almost entirely recorded traces and eval data; the code surface is 13 files.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

MoA now supports single-model fallback routing, structured worker-pool assembly, truncation-aware text arbitration, optional refinement, asymmetric tool handling, and expanded replay and OpenRouter evaluation coverage.

Changes

MoA routing and worker-pool assembly

Layer / File(s) Summary
Degraded routing
crates/mesh-llm-host-runtime/src/network/openai/ingress.rs, crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
MoA can rewrite model: "mesh" to an advertised real model and continue through ordinary routing. MoA-handled requests still terminate normally.
Worker-pool construction and gateway configuration
crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs, crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs, crates/mesh-mixture-of-agents/src/worker.rs
The gateway groups aliases, resolves reachable backends, applies tier admission, ranks actor candidates, limits committee size, configures patience profiles, and disables worker thinking.

Mixture-of-agents orchestration

Layer / File(s) Summary
Backend replies and context packing
crates/mesh-mixture-of-agents/src/backend.rs, crates/mesh-mixture-of-agents/src/normalize.rs, crates/mesh-mixture-of-agents/src/context.rs, crates/mesh-mixture-of-agents/src/fanout.rs
Backend replies now preserve truncation state. Text and tool turns use different prompts and payload budgets. Fan-out supports qualification thresholds, refinement-aware grace expiry, and advisory gathering.
Text arbitration and refinement
crates/mesh-mixture-of-agents/src/arbiter.rs, crates/mesh-mixture-of-agents/src/refinement.rs, crates/mesh-mixture-of-agents/src/reducer.rs
Arbitration is text-only. Truncated or disagreeing answers escalate to synthesis. Optional cross-peer refinement uses bounded, best-effort worker rounds and host-provided actor candidates.
Asymmetric tool turns
crates/mesh-mixture-of-agents/src/lib.rs, crates/mesh-mixture-of-agents/src/tool_turn.rs
Tool queries select an actor model and may gather tool-free advisory references. The actor receives real tool schemas and applies forced-tool and failure fallbacks.

Validation and evaluation

Layer / File(s) Summary
Integration and replay validation
crates/mesh-mixture-of-agents/tests/*.rs, crates/mesh-mixture-of-agents/tests/fixtures/*
Tests cover partial worker failure, truncation handling, tool-call preservation, refinement timing, fallback behavior, and gateway configuration defaults.
OpenRouter evaluation tooling
evals/moa-openrouter/*.py, evals/moa-openrouter/*.jsonl, evals/moa-openrouter/README.md, evals/moa-openrouter/RESULTS.md
The PR adds OpenRouter recording, replay-fixture generation, ablation analysis, agentic and corpus traces, evaluation scripts, and documented results.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • Mesh-LLM/mesh-llm#820: Modifies overlapping MoA gateway, arbitration, fan-out, context, and worker-selection code.
  • Mesh-LLM/mesh-llm#823: Revises the same MoA gateway and mixture-of-agents routing paths.
  • Mesh-LLM/mesh-llm#837: Changes overlapping gateway, arbitration, fan-out, refinement, and truncation behavior.

Suggested labels: experimental

Suggested reviewers: ndizazzo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly relates to the MoA rebuild and its focus on improving mesh answers as more capable nodes participate.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/moa-synthesis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request is currently a draft. Reviews will not take place until the PR is marked as ready for review.

@michaelneale michaelneale changed the title MoA: give workers tools, synthesize disagreement, survive flaky peers MoA: asymmetric tool turns — best tool-caller acts, references advise Jul 31, 2026
@michaelneale michaelneale changed the title MoA: asymmetric tool turns — best tool-caller acts, references advise MoA: a small-model mesh that beats its best member Aug 2, 2026
@michaelneale michaelneale changed the title MoA: a small-model mesh that beats its best member MoA: refinement-based collaboration for small-model meshes Aug 3, 2026
@michaelneale michaelneale changed the title MoA: refinement-based collaboration for small-model meshes MoA: ensembling that improves as mesh nodes join Aug 4, 2026
@michaelneale
michaelneale marked this pull request as ready for review August 4, 2026 04:14
@github-actions
github-actions Bot requested a review from i386 August 4, 2026 04:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (18)
evals/moa-openrouter/probe_tools.py (1)

65-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Manage the output file handles in both eval scripts. Both scripts open a JSONL output file without a context manager, so no exit path guarantees a close. The shared fix is to scope each handle with with open(...).

  • evals/moa-openrouter/probe_tools.py#L65-L70: move the module-level open("fanout.jsonl", "a") into main and scope it with with, so importing the module no longer creates the file.
  • evals/moa-openrouter/record_agentic.py#L236-L293: replace out = open("agentic.jsonl", "w") plus out.close() with a with open(...) block, so an exception during a scenario still closes the file.
🤖 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 `@evals/moa-openrouter/probe_tools.py` around lines 65 - 70, Manage both output
file handles with context managers: in evals/moa-openrouter/probe_tools.py lines
65-70, move the module-level open call into main and scope it with with; in
evals/moa-openrouter/record_agentic.py lines 236-293, replace the out handle and
explicit close with a with open block covering scenario processing. Ensure
importing probe_tools.py does not create the output file and exceptions still
close both handles.
evals/moa-openrouter/record_agentic.py (1)

275-291: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use iterable unpacking for the message list.

Ruff reports RUF005 here. Replace the list concatenation with unpacking.

♻️ Proposed refactor
-                messages = messages + [
+                messages = [
+                    *messages,
                     {
                         "role": "assistant",
🤖 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 `@evals/moa-openrouter/record_agentic.py` around lines 275 - 291, Update the
message accumulation expression in the agentic recording flow to use iterable
unpacking for the existing messages and the two new message dictionaries,
replacing list concatenation while preserving their order and contents.

Source: Linters/SAST tools

evals/moa-openrouter/make_fixture.py (2)

23-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant int call.

round() with a single argument already returns an int. Ruff reports this as RUF046.

♻️ Proposed change
-        "elapsed_ms": int(round((w["elapsed"] or 0) * 1000)),
+        "elapsed_ms": round((w["elapsed"] or 0) * 1000),
🤖 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 `@evals/moa-openrouter/make_fixture.py` at line 23, Update the elapsed_ms
expression in the fixture-building logic to remove the redundant int wrapper and
rely on round((w["elapsed"] or 0) * 1000) returning an integer, preserving the
existing elapsed fallback and rounding behavior.

Source: Linters/SAST tools


14-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve the input and output paths relative to the script.

OUT and both open calls use paths relative to the current working directory. The script writes the fixture to the wrong location, or fails, when it runs from the repository root. The docstring does not state the required working directory. The two input handles also stay open until interpreter exit.

Anchor the paths to __file__ and use context managers.

♻️ Proposed change
 import json
+from pathlib import Path
 
-OUT = "../../crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json"
+HERE = Path(__file__).resolve().parent
+OUT = HERE.parents[1] / "crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json"
-for line in open("agentic.jsonl"):
-    line = line.strip()
-    if not line:
-        continue
-    r = json.loads(line)
-    cases.append(
+with open(HERE / "agentic.jsonl") as fh:
+    agentic_lines = fh.read().splitlines()
+for line in agentic_lines:
+    line = line.strip()
+    if not line:
+        continue
+    r = json.loads(line)
+    cases.append(

Apply the same change to the corpus.jsonl loop at line 53.

Also applies to: 34-34, 53-53, 71-72

🤖 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 `@evals/moa-openrouter/make_fixture.py` around lines 14 - 16, Update the
fixture path setup in the script to resolve OUT and both input paths relative to
the script’s __file__, so execution is independent of the current working
directory. Wrap every input and output open call, including the corpus.jsonl
loop, in context managers to close handles promptly.
crates/mesh-mixture-of-agents/tests/eval_openrouter.rs (1)

1453-1459: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Arm C can get one candidate while arm B gets none.

diverse.len().max(1) forces at least one homogeneous candidate. If every diverse peer call fails, arm B runs with zero candidates and is identical to arm A, while arm C still receives one candidate. The reported B-C differential then measures candidate presence, not family diversity. Match the counts exactly.

♻️ Proposed change
-            for _ in 0..diverse.len().max(1) {
+            for _ in 0..diverse.len() {
                 if let Some(c) = structured_proposal(&backend, &finalizer, task).await {
                     homo.push(c);
                 }
             }
🤖 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/eval_openrouter.rs` around lines 1453 -
1459, Update the homogeneous candidate loop in the arm C setup to generate
exactly diverse.len() candidates, removing the max(1) fallback. Preserve the
existing structured_proposal failure handling so zero diverse candidates
produces zero homogeneous candidates and the B-C comparison measures family
diversity.
crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs (1)

213-217: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the stale comment on enable_thinking.

The comment says try_handle_moa overrides this value "when the caller has expressed a preference". That is no longer true. effective_enable_thinking_for_moa now always returns Some(false) (lines 17-46), and try_handle_moa assigns it unconditionally. A reader following this comment would look for caller-driven behavior that no longer exists.

📝 Proposed wording
-        // Defaults to leaving each model's thinking behavior alone.
-        // `try_handle_moa` overrides this from the inbound request body
-        // when the caller has expressed a preference
-        // (`reasoning_effort: "none"`, `enable_thinking: false`, etc.).
-        enable_thinking: None,
+        // Placeholder: `try_handle_moa` always overwrites this with
+        // `effective_enable_thinking_for_moa`, which is `Some(false)` as a
+        // policy. Caller reasoning knobs are parsed for logging only.
+        enable_thinking: None,
🤖 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-llm-host-runtime/src/network/openai/moa_gateway/workers.rs`
around lines 213 - 217, Update the comment above enable_thinking to reflect that
effective_enable_thinking_for_moa always returns Some(false) and try_handle_moa
assigns that value unconditionally; remove the outdated claim that inbound
caller preferences control the override.
crates/mesh-mixture-of-agents/src/normalize.rs (1)

55-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider passing truncation into the normalizer.

Every path in this file hardcodes truncated: false, and each caller must remember to stamp the real value after the call (fanout::gather_workers_incremental, fanout::gather_references, refinement::refine_round). A new call site that omits the stamp silently reports a truncated answer as complete, which is the exact failure this field exists to prevent. Accepting the flag as a parameter of normalize_worker_output would make the compiler enforce it.

🤖 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/normalize.rs` around lines 55 - 60, Update
normalize_worker_output to accept a truncation flag parameter and use it when
constructing WorkerOutput instead of hardcoding truncated: false. Pass the
actual flag from every caller, including fanout::gather_workers_incremental,
fanout::gather_references, and refinement::refine_round, then remove their
post-call stamping.
crates/mesh-mixture-of-agents/src/backend.rs (1)

155-159: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The retry removes caller-supplied chat_template_kwargs content.

apply_enable_thinking merges enable_thinking into any existing chat_template_kwargs object. The retry removes the whole chat_template_kwargs key, so template options the caller set for other purposes are lost on the retried request. Removing only the thinking keys keeps the rest intact.

♻️ Proposed change
                 let mut retry_body = body.clone();
                 if let Some(obj) = retry_body.as_object_mut() {
                     obj.remove("reasoning_effort");
-                    obj.remove("chat_template_kwargs");
+                    if let Some(kwargs) = obj
+                        .get_mut("chat_template_kwargs")
+                        .and_then(Value::as_object_mut)
+                    {
+                        kwargs.remove("enable_thinking");
+                        if kwargs.is_empty() {
+                            obj.remove("chat_template_kwargs");
+                        }
+                    }
                 }
🤖 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/backend.rs` around lines 155 - 159, The
retry preparation in the backend request flow must preserve caller-supplied
chat_template_kwargs. Update the retry_body cleanup to remove only the
thinking-related entries added by apply_enable_thinking, while retaining the
rest of the chat_template_kwargs object; keep removal of reasoning_effort
unchanged.
crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs (1)

34-42: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider the token requirement and a deterministic pick when degrading.

degrade_to_single_model takes the first model returned by models_being_served(). Two effects follow:

  • The picked model may not satisfy required_tokens. build_moa_config applies context_selection::context_can_satisfy per worker, but this fallback path applies no context check, so a request that needs a large context can be routed to a small-context model.
  • The iteration order of models_being_served() decides the answer. If that order is not stable, the same lone node can answer with different models across requests.

A deterministic selection that prefers locally served models with sufficient context would make the degraded path predictable.

🤖 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-llm-host-runtime/src/network/openai/moa_gateway/mod.rs` around
lines 34 - 42, Update the fallback model selection around
degrade_to_single_model to filter out the virtual model and retain only models
whose context satisfies required_tokens, preferring locally served models when
available. Sort or otherwise select from the remaining candidates
deterministically before choosing the target, while preserving the existing 503
response when no eligible model exists.
crates/mesh-mixture-of-agents/src/fanout.rs (1)

350-363: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the reference stop condition.

Line 352 checks !outputs.is_empty() && outputs.len() >= min_references. The first clause only matters when min_references == 0, and the only caller passes dispatched.len().div_ceil(2).max(1). Either drop the redundant clause or clamp min_references with .max(1) inside this function so the intent is local.

🤖 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 350 - 363, In the
reference collection loop, simplify the stop condition around outputs.len() and
min_references: remove the redundant outputs non-empty check, or normalize
min_references to at least one within the function before the loop. Preserve the
behavior that collection stops once the configured minimum number of references
is reached.
crates/mesh-llm-host-runtime/src/network/openai/ingress.rs (1)

842-857: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Set the degraded flag in the match arm instead of comparing model values.

Line 856 infers degraded from routing_model != decision.effective_model. The branch that produced the value is already known at line 847. The comparison holds today only because degrade_to_single_model excludes moa::VIRTUAL_MODEL_NAME, so the rewritten name can never equal "mesh". If that exclusion changes, the pipeline classifier would run against a stale decision. A flag set in the arm removes the dependency.

♻️ Proposed refactor
-    let mut routing_model = decision.effective_model.clone();
-    let tcp_stream = match try_handle_moa_intercept(tcp_stream, &mut request, &ctx, &decision).await
-    {
-        MoaInterceptResult::Handled => return,
-        MoaInterceptResult::NotMoa(stream) => stream,
-        MoaInterceptResult::Degraded { stream, model } => {
-            routing_model = model;
-            stream
-        }
-    };
-
-    let mut tcp_stream = tcp_stream;
-    // A degraded turn is a plain single-model request; skip the pipeline
-    // classifier (computed against "mesh") and route it directly.
-    let degraded = routing_model != decision.effective_model;
+    let mut routing_model = decision.effective_model.clone();
+    let mut degraded = false;
+    let mut tcp_stream =
+        match try_handle_moa_intercept(tcp_stream, &mut request, &ctx, &decision).await {
+            MoaInterceptResult::Handled => return,
+            MoaInterceptResult::NotMoa(stream) => stream,
+            MoaInterceptResult::Degraded { stream, model } => {
+                routing_model = model;
+                degraded = true;
+                stream
+            }
+        };
+
+    // A degraded turn is a plain single-model request; skip the pipeline
+    // classifier (computed against "mesh") and route it directly.
🤖 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-llm-host-runtime/src/network/openai/ingress.rs` around lines 842
- 857, Update the request-routing flow around try_handle_moa_intercept to
initialize a degraded flag before the match and set it explicitly in the
MoaInterceptResult::Degraded arm. Replace the routing_model !=
decision.effective_model comparison with that flag, while leaving the Handled
and NotMoa paths unchanged so pipeline classification is skipped whenever the
intercept produced a degraded single-model request.
crates/mesh-mixture-of-agents/src/context.rs (1)

532-545: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the advisor payload bound.

reducer_payload_budget and REFINEMENT_DRAFT_BUDGET express their limits as named items with the reasoning attached. This loop hardcodes 500 and the derived 497. Introduce a constant, for example ADVICE_PAYLOAD_BUDGET, and derive the truncation length from it so the two values cannot drift.

♻️ Proposed refactor
+/// How much of each advisor's prose the actor may see.
+const ADVICE_PAYLOAD_BUDGET: usize = 500;
+
@@
-            let payload = if r.payload.len() > 500 {
-                format!("{}...", crate::worker::truncate_chars(&r.payload, 497))
+            let payload = if r.payload.len() > ADVICE_PAYLOAD_BUDGET {
+                format!(
+                    "{}...",
+                    crate::worker::truncate_chars(&r.payload, ADVICE_PAYLOAD_BUDGET - 3)
+                )
             } else {
                 r.payload.clone()
             };
🤖 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/context.rs` around lines 532 - 545, In the
advisor payload loop over references, replace the hardcoded 500-byte/character
bound and derived 497 truncation length with a named constant such as
ADVICE_PAYLOAD_BUDGET, documenting the limit’s purpose consistently with
reducer_payload_budget and REFINEMENT_DRAFT_BUDGET. Derive the truncation length
from that constant so the displayed payload plus ellipsis always stays within
the same budget.
crates/mesh-mixture-of-agents/src/tool_turn.rs (2)

255-336: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for actor_body.

The policy tests are thorough. actor_body is untested, and it encodes the response contract for the whole tool path: a ToolProposal must emit tool_calls, an Uncertainty with a forced tool must emit the forced call, and an Uncertainty with no forced tool and no references must emit MOA_ERR_NO_USABLE_ANSWER. It is a pure function over WorkerOutput, so it needs no backend. Add cases for those three branches.

🤖 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/tool_turn.rs` around lines 255 - 336, Add
unit tests for the pure actor_body function covering three response-contract
branches: ToolProposal produces tool_calls, Uncertainty with a forced tool
produces that forced call, and Uncertainty without a forced tool or references
returns MOA_ERR_NO_USABLE_ANSWER. Construct WorkerOutput fixtures directly
without backend setup, and keep the existing policy tests unchanged.

143-147: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Both new fan-out modules rebuild an identical prompt inside the per-worker loop. dispatch_and_gather_references and refine_round each call a context packer once per spawned worker with arguments that do not vary by worker, so the same PackedContext is built N times. Hoist the packing above the loop and clone the messages into each task.

  • crates/mesh-mixture-of-agents/src/tool_turn.rs#L143-L147: move context::pack_for_reference(session, REFERENCE_HISTORY_MESSAGES) above the for a in &assignments loop and clone packed.messages per task.
  • crates/mesh-mixture-of-agents/src/refinement.rs#L100-L102: move context::pack_for_refinement(session, &texts) above the loop; this call also concatenates and truncates every draft, so the repeated work is larger here.
🤖 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/tool_turn.rs` around lines 143 - 147, The
fan-out paths rebuild identical packed context for every worker. In
crates/mesh-mixture-of-agents/src/tool_turn.rs lines 143-147, update
dispatch_and_gather_references to call context::pack_for_reference once before
the for a in &assignments loop and clone packed.messages into each task; in
crates/mesh-mixture-of-agents/src/refinement.rs lines 100-102, update
refine_round to call context::pack_for_refinement once before its worker loop
and clone the resulting messages per task.
crates/mesh-mixture-of-agents/src/refinement.rs (2)

194-291: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a refine_round test for the shortfall path.

The tests cover policy and budget arithmetic well. They do not exercise refine_round, whose central safety claim is that fewer than MIN_DRAFTS refinements return None so the caller keeps the round-1 outputs. reducer.rs already has a FakeBackend helper that can drive this. Add one test where a single worker succeeds and assert refine_round returns None.

🤖 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/refinement.rs` around lines 194 - 291, Add
a unit test for refine_round using the existing reducer.rs FakeBackend helper,
configuring one worker to succeed and produce fewer than MIN_DRAFTS refinement
results. Assert that refine_round returns None, preserving the caller’s round-1
outputs; keep the test focused on this shortfall path.

141-148: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Record a summary when a refinement reply is empty.

Line 142 skips a reply whose text is blank and continues without pushing a WorkerSummary. The worker ran and consumed tokens, but it appears in no accounting. Every other outcome in this loop produces a summary, and the round-1 gather reconciles all dispatched workers. Add a failed summary so refinement attempts stay observable.

♻️ Proposed change
                     Ok((model, role, Ok(reply), elapsed)) => {
                         if reply.text.trim().is_empty() {
+                            summaries.push(WorkerSummary {
+                                model,
+                                role,
+                                succeeded: false,
+                                elapsed_ms: elapsed,
+                                output_kind: None,
+                                confidence: None,
+                            });
                             continue;
                         }
🤖 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/refinement.rs` around lines 141 - 148,
Update the empty-reply branch in the refinement result loop before continue so
it pushes a failed WorkerSummary for the completed worker, preserving the worker
identity and relevant timing/token accounting; then continue without normalizing
output. Ensure every dispatched refinement attempt remains represented
consistently with the other result branches and round-1 reconciliation.
crates/mesh-mixture-of-agents/src/lib.rs (2)

1309-1309: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting the response builders into their own module.

These six builders are now pub(crate) because tool_turn consumes them. They form a separable responsibility: OpenAI wire-response construction. lib.rs is over 2,000 lines and this PR modifies it.

Move best_answer, fallback_worker_response, tool_proposal_response, error_response, chat_response, and tool_call_response plus response_builder_tests into a response module. That keeps lib.rs shrinking as the crate grows and moves the tests with the behavior.

As per coding guidelines: "When modifying a Rust source file over 1,000 lines, extract any separable responsibility into a semantically named module, keep the new file under 1,000 lines, and move relevant tests with the extracted behavior."

Also applies to: 1326-1326, 1338-1338, 1374-1374, 1420-1420, 1434-1434

🤖 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/lib.rs` at line 1309, Extract the OpenAI
wire-response builders best_answer, fallback_worker_response,
tool_proposal_response, error_response, chat_response, and tool_call_response
from lib.rs into a semantically named response module, preserving their
pub(crate) visibility and existing behavior. Move response_builder_tests into
the same module, update module declarations and call sites such as tool_turn,
and keep the new module under 1,000 lines.

Source: Coding guidelines


316-350: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the now-unreachable tool bookkeeping on the text path.

The guard at line 310 returns for every turn where has_tools is true or forced_tool is set. After that guard, query_uses_tools at line 343 is always false, selected_tool_names is always empty, and grace_mode_for_turn(session, has_tools) is always called with has_tools == false, so GraceMode::Tool is unreachable here. The comment at lines 318-342 explains a decision that this path no longer makes.

Replace the computed values with constants and move the historical rationale to tool_turn. The change keeps behavior identical and removes a misleading contract for the next reader.

♻️ Proposed simplification
-    let query_uses_tools = forced_tool.is_some() || has_tools;
-    let selected_tool_names = if let Some(tool) = forced_tool {
-        vec![tool.name.clone()]
-    } else if query_uses_tools {
-        selected_tool_names_for_turn(session, allowed_tools)
-    } else {
-        Vec::new()
-    };
+    // Tool-bearing turns returned above, so this path is always tool-free.
+    let query_uses_tools = false;
+    let selected_tool_names: Vec<String> = Vec::new();
🤖 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/lib.rs` around lines 316 - 350, In the
post-guard text path, replace the computed tool state with constant no-tools
values and call grace_mode_for_turn with tools unavailable, since forced_tool
and has_tools cannot reach this branch. Move the historical tool-availability
rationale from this block to tool_turn, retaining only comments relevant to the
text path. Preserve the existing behavior while removing the misleading
query_uses_tools and selected_tool_names contract.
🤖 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-llm-host-runtime/src/network/openai/moa_gateway/workers.rs`:
- Around line 399-470: Extract the separable pool-assembly responsibilities from
this oversized module into semantically named modules, such as admission and
patience, keeping each new file under 1,000 lines. Move apply_admission_control
and its tests into the admission module; move cap_committee,
self_fill_from_extra_instances, assemble_worker_pool, and their tests into an
appropriate pool/committee module, and move PatienceProfile, patience_profile,
and related tests into the patience module. Update module declarations and call
sites while preserving existing behavior.
- Around line 522-548: Update assemble_worker_pool and
self_fill_from_extra_instances to carry the initial remote peer selected by
add_worker_backend, and skip that peer when adding extra instances to the MoA
committee. Pass the tracked peer through the call chain, remove the unused _http
parameter from self_fill_from_extra_instances and its callers, and preserve the
existing worker-count limit.

In `@crates/mesh-mixture-of-agents/src/arbiter.rs`:
- Around line 455-463: Update the match in single_output_decision so
OutputKind::ToolProposal returns Decision::NeedsReducer with the appropriate
reducer reason, alongside OutputKind::Uncertainty. Keep non-tool, non-uncertain
outputs on the existing Decision::Answer path.

In `@crates/mesh-mixture-of-agents/src/backend.rs`:
- Around line 356-361: Update the tool-proposal path around
BackendReply::complete to validate the raw args as JSON and set the reply’s
truncated state when parsing fails specifically with finish_reason == "length";
preserve the complete status for valid JSON or non-length finish reasons, and
avoid treating the unwrap_or("{}") fallback as proof that the original arguments
were complete.

In `@crates/mesh-mixture-of-agents/src/context.rs`:
- Around line 347-453: Extract the packing responsibilities from context.rs into
semantically named modules: move REFERENCE_PREAMBLE and pack_for_reference with
their tests to a reference module, pack_for_refinement and
REFINEMENT_DRAFT_BUDGET with related tests to refinement_context, and
pack_for_actor with its tests to actor. Update module declarations, imports, and
call sites so the existing behavior and APIs remain unchanged while keeping each
source file under 1,000 lines.
- Around line 475-497: Verify the refinement configuration in the code and
measurement setup around the PackedContext construction and its max_tokens
value. If the measured refinement configuration used the full worker token
budget rather than 1024 tokens, increase max_tokens to that budget so synthesis
is not unnecessarily truncated; otherwise preserve the current value.

In `@crates/mesh-mixture-of-agents/src/fanout.rs`:
- Around line 383-397: Update reconcile_dispatched, used by gather_references
and gather_workers_incremental, to match returned summaries to dispatched
workers by slot index or another unique per-dispatch identifier instead of model
name. Propagate that identifier through the dispatch/result flow so duplicate
same-model slots reconcile independently and failed or cancelled slots remain in
TurnResult.worker_summaries.

In `@crates/mesh-mixture-of-agents/src/reducer.rs`:
- Around line 154-158: Make reducer truncation observable in the output handling
around the result mapping that returns `(name, result.map(|reply| reply.text))`.
Preserve the full reducer text, but inspect `reply.truncated` and log when the
final synthesis response was truncated, using the existing logging mechanism and
context available in the reducer flow.

In `@crates/mesh-mixture-of-agents/src/tool_turn.rs`:
- Around line 178-187: Cap the optional advisory wait in the tool-turn flow
before calling gather_references, using the same worker-budget fraction
established by refinement.rs rather than the full config.worker_timeout. Keep
min_references unchanged and preserve pack_for_actor’s empty-reference fallback
when the bounded advisory round expires.
- Around line 79-92: Update the TurnResult construction in finalize_actor_output
so reducer_used reflects actor_ok instead of always being true. Preserve the
existing reducer_attempts value and response handling, ensuring failed reducer
attempts that return fallback_worker_response or error_response report
reducer_used as false.
- Around line 54-55: Update handle_tool_query to accept and propagate the
caller’s has_tools flag, passing it to context::pack_for_actor instead of
hardcoding true. Ensure tool_proposal_response and forced tool_call_response
paths use the same flag so tool-call responses are omitted when the request has
no tools array.

In `@crates/mesh-mixture-of-agents/src/worker.rs`:
- Around line 144-181: Align the documentation for canonical_base_name and
pool_is_homogeneous with the actual behavior: quant-tagged names remain distinct
from untagged names, so mixed quant variants are not considered homogeneous and
refinement::refinement_expected will not enable refinement for them. Update the
comments to remove the claim that quant variants share a base, and add coverage
for a plain name versus a quant-tagged alias to pin this contract.

In `@crates/mesh-mixture-of-agents/tests/eval_openrouter.rs`:
- Around line 2212-2215: Update the solo-baseline setup around small_mesh_pool
so it does not claim pool[0] is the strongest member: either select the
strongest model explicitly or revise the comment and output wording to describe
the first declaration-order entry. Also handle an empty pool before indexing,
using the test’s existing failure or early-return convention instead of allowing
pool[0] to panic.
- Around line 460-466: Replace the local truncate helper and its call sites with
the exported truncate_chars behavior, ensuring truncation never slices through a
UTF-8 character. Update crates/mesh-mixture-of-agents/src/lib.rs::truncate_chars
if necessary so its limit semantics match the required character-count boundary,
and apply the safe helper to model output, HTTP error bodies, and advisor
errors.

In `@crates/mesh-mixture-of-agents/tests/sim_real_traces.rs`:
- Around line 541-557: Update
crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L541-L557 to remove the
result.reducer_used skip for tool-bearing turns so the majority-argument
assertions execute; update is_reducer_call at
crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L146-L154 to recognize
the pack_for_actor system marker “## Advice from other models” and return
SYNTHESIZED for the actor call instead of replaying a worker body.

In `@crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs`:
- Around line 3-7: Update the module-level rationale near the refinement
description to remove the withdrawn “42/66/12, p=5.2e-05” result or replace it
with the controlled evaluation result from evals/moa-openrouter/RESULTS.md;
retain the explanation that refinement is best-effort and may introduce another
hanging fan-out.

In `@evals/moa-openrouter/probe_tools.py`:
- Around line 100-105: Update the tool-call formatting comprehension in the
result-printing block to read arguments defensively via the nested function
mapping, defaulting missing or falsy values to "{}" as in record_agentic.py,
while preserving the existing call-name formatting and probe flow.

In `@evals/moa-openrouter/record.py`:
- Around line 31-41: Update the worker record construction around the response
handling to persist the complete OpenAI response, including usage and unselected
fields, by storing resp directly in each record while retaining the existing
derived fields as needed.

In `@evals/moa-openrouter/RESULTS.md`:
- Around line 487-493: Update the documented merge-blocker status around the
harness-versus-production parity claim in moa::handle_turn: either provide
evidence that the orchestration gap is resolved, or explicitly record an
approved waiver. Do not present the production reasoning-quality improvement as
validated while this blocker remains unresolved.

---

Nitpick comments:
In `@crates/mesh-llm-host-runtime/src/network/openai/ingress.rs`:
- Around line 842-857: Update the request-routing flow around
try_handle_moa_intercept to initialize a degraded flag before the match and set
it explicitly in the MoaInterceptResult::Degraded arm. Replace the routing_model
!= decision.effective_model comparison with that flag, while leaving the Handled
and NotMoa paths unchanged so pipeline classification is skipped whenever the
intercept produced a degraded single-model request.

In `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs`:
- Around line 34-42: Update the fallback model selection around
degrade_to_single_model to filter out the virtual model and retain only models
whose context satisfies required_tokens, preferring locally served models when
available. Sort or otherwise select from the remaining candidates
deterministically before choosing the target, while preserving the existing 503
response when no eligible model exists.

In `@crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs`:
- Around line 213-217: Update the comment above enable_thinking to reflect that
effective_enable_thinking_for_moa always returns Some(false) and try_handle_moa
assigns that value unconditionally; remove the outdated claim that inbound
caller preferences control the override.

In `@crates/mesh-mixture-of-agents/src/backend.rs`:
- Around line 155-159: The retry preparation in the backend request flow must
preserve caller-supplied chat_template_kwargs. Update the retry_body cleanup to
remove only the thinking-related entries added by apply_enable_thinking, while
retaining the rest of the chat_template_kwargs object; keep removal of
reasoning_effort unchanged.

In `@crates/mesh-mixture-of-agents/src/context.rs`:
- Around line 532-545: In the advisor payload loop over references, replace the
hardcoded 500-byte/character bound and derived 497 truncation length with a
named constant such as ADVICE_PAYLOAD_BUDGET, documenting the limit’s purpose
consistently with reducer_payload_budget and REFINEMENT_DRAFT_BUDGET. Derive the
truncation length from that constant so the displayed payload plus ellipsis
always stays within the same budget.

In `@crates/mesh-mixture-of-agents/src/fanout.rs`:
- Around line 350-363: In the reference collection loop, simplify the stop
condition around outputs.len() and min_references: remove the redundant outputs
non-empty check, or normalize min_references to at least one within the function
before the loop. Preserve the behavior that collection stops once the configured
minimum number of references is reached.

In `@crates/mesh-mixture-of-agents/src/lib.rs`:
- Line 1309: Extract the OpenAI wire-response builders best_answer,
fallback_worker_response, tool_proposal_response, error_response, chat_response,
and tool_call_response from lib.rs into a semantically named response module,
preserving their pub(crate) visibility and existing behavior. Move
response_builder_tests into the same module, update module declarations and call
sites such as tool_turn, and keep the new module under 1,000 lines.
- Around line 316-350: In the post-guard text path, replace the computed tool
state with constant no-tools values and call grace_mode_for_turn with tools
unavailable, since forced_tool and has_tools cannot reach this branch. Move the
historical tool-availability rationale from this block to tool_turn, retaining
only comments relevant to the text path. Preserve the existing behavior while
removing the misleading query_uses_tools and selected_tool_names contract.

In `@crates/mesh-mixture-of-agents/src/normalize.rs`:
- Around line 55-60: Update normalize_worker_output to accept a truncation flag
parameter and use it when constructing WorkerOutput instead of hardcoding
truncated: false. Pass the actual flag from every caller, including
fanout::gather_workers_incremental, fanout::gather_references, and
refinement::refine_round, then remove their post-call stamping.

In `@crates/mesh-mixture-of-agents/src/refinement.rs`:
- Around line 194-291: Add a unit test for refine_round using the existing
reducer.rs FakeBackend helper, configuring one worker to succeed and produce
fewer than MIN_DRAFTS refinement results. Assert that refine_round returns None,
preserving the caller’s round-1 outputs; keep the test focused on this shortfall
path.
- Around line 141-148: Update the empty-reply branch in the refinement result
loop before continue so it pushes a failed WorkerSummary for the completed
worker, preserving the worker identity and relevant timing/token accounting;
then continue without normalizing output. Ensure every dispatched refinement
attempt remains represented consistently with the other result branches and
round-1 reconciliation.

In `@crates/mesh-mixture-of-agents/src/tool_turn.rs`:
- Around line 255-336: Add unit tests for the pure actor_body function covering
three response-contract branches: ToolProposal produces tool_calls, Uncertainty
with a forced tool produces that forced call, and Uncertainty without a forced
tool or references returns MOA_ERR_NO_USABLE_ANSWER. Construct WorkerOutput
fixtures directly without backend setup, and keep the existing policy tests
unchanged.
- Around line 143-147: The fan-out paths rebuild identical packed context for
every worker. In crates/mesh-mixture-of-agents/src/tool_turn.rs lines 143-147,
update dispatch_and_gather_references to call context::pack_for_reference once
before the for a in &assignments loop and clone packed.messages into each task;
in crates/mesh-mixture-of-agents/src/refinement.rs lines 100-102, update
refine_round to call context::pack_for_refinement once before its worker loop
and clone the resulting messages per task.

In `@crates/mesh-mixture-of-agents/tests/eval_openrouter.rs`:
- Around line 1453-1459: Update the homogeneous candidate loop in the arm C
setup to generate exactly diverse.len() candidates, removing the max(1)
fallback. Preserve the existing structured_proposal failure handling so zero
diverse candidates produces zero homogeneous candidates and the B-C comparison
measures family diversity.

In `@evals/moa-openrouter/make_fixture.py`:
- Line 23: Update the elapsed_ms expression in the fixture-building logic to
remove the redundant int wrapper and rely on round((w["elapsed"] or 0) * 1000)
returning an integer, preserving the existing elapsed fallback and rounding
behavior.
- Around line 14-16: Update the fixture path setup in the script to resolve OUT
and both input paths relative to the script’s __file__, so execution is
independent of the current working directory. Wrap every input and output open
call, including the corpus.jsonl loop, in context managers to close handles
promptly.

In `@evals/moa-openrouter/probe_tools.py`:
- Around line 65-70: Manage both output file handles with context managers: in
evals/moa-openrouter/probe_tools.py lines 65-70, move the module-level open call
into main and scope it with with; in evals/moa-openrouter/record_agentic.py
lines 236-293, replace the out handle and explicit close with a with open block
covering scenario processing. Ensure importing probe_tools.py does not create
the output file and exceptions still close both handles.

In `@evals/moa-openrouter/record_agentic.py`:
- Around line 275-291: Update the message accumulation expression in the agentic
recording flow to use iterable unpacking for the existing messages and the two
new message dictionaries, replacing list concatenation while preserving their
order and contents.
🪄 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: 1f1bf079-4ba5-464a-8bc2-9d17fd49c867

📥 Commits

Reviewing files that changed from the base of the PR and between 66c40d6 and 228a1ef.

📒 Files selected for processing (39)
  • crates/mesh-llm-guardrails/src/rescue.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
  • crates/mesh-mixture-of-agents/src/arbiter.rs
  • crates/mesh-mixture-of-agents/src/backend.rs
  • crates/mesh-mixture-of-agents/src/context.rs
  • crates/mesh-mixture-of-agents/src/fanout.rs
  • crates/mesh-mixture-of-agents/src/lib.rs
  • crates/mesh-mixture-of-agents/src/normalize.rs
  • crates/mesh-mixture-of-agents/src/reducer.rs
  • crates/mesh-mixture-of-agents/src/refinement.rs
  • crates/mesh-mixture-of-agents/src/tool_guard.rs
  • crates/mesh-mixture-of-agents/src/tool_turn.rs
  • crates/mesh-mixture-of-agents/src/worker.rs
  • crates/mesh-mixture-of-agents/tests/eval_openrouter.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/ablation_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/committee_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json
  • crates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rs
  • crates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rs
  • crates/mesh-mixture-of-agents/tests/sim_partial_worker_survival.rs
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs
  • crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs
  • crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rs
  • crates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
  • evals/moa-openrouter/README.md
  • evals/moa-openrouter/RESULTS.md
  • evals/moa-openrouter/agentic.jsonl
  • evals/moa-openrouter/analyze_ablation.py
  • evals/moa-openrouter/corpus.jsonl
  • evals/moa-openrouter/fanout.jsonl
  • evals/moa-openrouter/make_fixture.py
  • evals/moa-openrouter/orclient.py
  • evals/moa-openrouter/probe_tools.py
  • evals/moa-openrouter/record.py
  • evals/moa-openrouter/record_agentic.py

Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs Outdated
Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs Outdated
Comment on lines +455 to 463
// Tool turns take the asymmetric actor path, so this text-path arbiter
// never sees an executable ToolProposal (tool-shaped text is demoted to
// Uncertainty by `enforce_tool_call_contract` when tools are disabled).
match output.kind {
OutputKind::ToolProposal if has_tools => {
if let Some(ref name) = output.tool_name {
Decision::ToolCall {
name: name.clone(),
arguments: output
.tool_arguments
.clone()
.unwrap_or(Value::Object(Default::default())),
}
} else {
Decision::Answer(output.payload.clone())
}
}
OutputKind::Uncertainty => Decision::NeedsReducer {
reason: "single worker uncertain".into(),
},
_ => Decision::Answer(output.payload.clone()),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm empty-allow-list behaviour of enforce_tool_call_contract.
fd -t f 'tool_guard.rs' crates -x sed -n '1,80p'

Repository: Mesh-LLM/mesh-llm

Length of output: 2911


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant arbiter implementation and nearby tests without running repo code.
fd -t f 'arbiter.rs' crates/mesh-mixture-of-agents/src -x sh -c '
  echo "=== $1 ==="
  wc -l "$1"
  echo "--- outline ---"
  ast-grep outline "$1" || true
  echo "--- relevant sections ---"
  sed -n "420,490p" "$1"
  echo "--- empty_allowed_list references in arbiter ---"
  rg -n "enforce_tool_call_contract|single_output_decision|arbitrate|ToolProposal|OutputKind::" "$1" || true
' sh {}

echo "=== tool_guard tests ==="
fd -t f 'tool_guard.rs' crates -x sh -c '
  sed -n "60,160p" "$1"
' sh {}

Repository: Mesh-LLM/mesh-llm

Length of output: 13581


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the multi-output branch and make sure ToolProposal is absent from answer/critique/uncertainty collections.
fd -t f 'arbiter.rs' crates/mesh-mixture-of-agents/src -x sh -c '
  echo "=== $1 relevant branch ==="
  sed -n "33,75p" "$1"
  echo "--- ToolProposal/Arbitrate tests ---"
  rg -n "ToolProposal|empty_allowed_list_is_noop|enforce_tool_call_contract|arbitrate" "$1" || true
' sh {}

# Read-only behavioral probe of the relevant guard condition and decision arms from the source text.
python3 - <<'PY'
from pathlib import Path
arbiter = Path("crates/mesh-mixture-of-agents/src/arbiter.rs").read_text()
for pattern in [
    "let allowed_tools = enforce_tool_call_contract",
    "let mut allowed_tools = allowed_tools.clone()",
    "enforce_tool_call_contract(&mut out, allowed_tools",
]:
    print(f"{pattern!r}: {'present' if pattern in arbiter else 'absent'}")
print("single_output_decision guard references allowed_tools:", "allowed_tools" in arbiter[arbiter.find("fn single_output_decision"):arbiter.find("\n}", arbiter.find("fn single_output_decision"))])
PY

Repository: Mesh-LLM/mesh-llm

Length of output: 2795


Route lone ToolProposal outputs to the reducer.

enforce_tool_call_contract leaves an empty allow-list unchanged, and this arbiter has no allowed_tools guard. A single tool-shaped payload reaches single_output_decision as OutputKind::ToolProposal and falls into the _ arm as Decision::Answer(payload), returning proposal prose to a tool-free request. Send ToolProposal to NeedsReducer so synthesis produces real text.

🐛 Proposed fix
-    // Tool turns take the asymmetric actor path, so this text-path arbiter
-    // never sees an executable ToolProposal (tool-shaped text is demoted to
-    // Uncertainty by `enforce_tool_call_contract` when tools are disabled).
+    // Tool turns take the asymmetric actor path, so a `ToolProposal` here is
+    // tool-shaped text on a tools-disabled turn. Its payload is not an answer,
+    // so synthesize instead of returning the proposal prose verbatim.
     match output.kind {
-        OutputKind::Uncertainty => Decision::NeedsReducer {
+        OutputKind::Uncertainty => Decision::NeedsReducer {
             reason: "single worker uncertain".into(),
         },
+        OutputKind::ToolProposal => Decision::NeedsReducer {
+            reason: "single worker proposed a tool on a tool-free turn".into(),
+        },
         _ => Decision::Answer(output.payload.clone()),
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Tool turns take the asymmetric actor path, so this text-path arbiter
// never sees an executable ToolProposal (tool-shaped text is demoted to
// Uncertainty by `enforce_tool_call_contract` when tools are disabled).
match output.kind {
OutputKind::ToolProposal if has_tools => {
if let Some(ref name) = output.tool_name {
Decision::ToolCall {
name: name.clone(),
arguments: output
.tool_arguments
.clone()
.unwrap_or(Value::Object(Default::default())),
}
} else {
Decision::Answer(output.payload.clone())
}
}
OutputKind::Uncertainty => Decision::NeedsReducer {
reason: "single worker uncertain".into(),
},
_ => Decision::Answer(output.payload.clone()),
}
// Tool turns take the asymmetric actor path, so a `ToolProposal` here is
// tool-shaped text on a tools-disabled turn. Its payload is not an answer,
// so synthesize instead of returning the proposal prose verbatim.
match output.kind {
OutputKind::Uncertainty => Decision::NeedsReducer {
reason: "single worker uncertain".into(),
},
OutputKind::ToolProposal => Decision::NeedsReducer {
reason: "single worker proposed a tool on a tool-free turn".into(),
},
_ => Decision::Answer(output.payload.clone()),
}
🤖 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 455 - 463, Update
the match in single_output_decision so OutputKind::ToolProposal returns
Decision::NeedsReducer with the appropriate reducer reason, alongside
OutputKind::Uncertainty. Keep non-tool, non-uncertain outputs on the existing
Decision::Answer path.

Comment thread crates/mesh-mixture-of-agents/src/backend.rs
Comment thread crates/mesh-mixture-of-agents/src/context.rs
Comment on lines +541 to +557
// The reducer may legitimately rewrite the turn into prose; only
// assert when a tool call was emitted from worker proposals.
if tools.is_empty() || result.reducer_used {
continue;
}

let majority_val: Value = serde_json::from_str(&majority).unwrap_or(Value::Null);
for (name, args) in &tools {
let got: Value = serde_json::from_str(args).unwrap_or(Value::Null);
assert_eq!(
got, majority_val,
"case `{}`: tool `{name}` should use the majority arguments \
({majority_n} workers proposed {majority}), not a minority variant. \
Got {args}",
case.id
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The replay harness still models the symmetric reducer path for tool turns. Tool-bearing turns now route to handle_tool_query, which packs the final request with pack_for_actor and always reports reducer_used: true. Both sites assume the old reducer fan-out shape, so the tool assertions no longer measure the shipped path.

  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L541-L557: replace the result.reducer_used skip, which is always true for has_tools cases and makes the majority-argument assert_eq! unreachable.
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L146-L154: extend is_reducer_call to recognize the pack_for_actor system marker (## Advice from other models), so the actor call is answered with SYNTHESIZED instead of a replayed worker body.
📍 Affects 1 file
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L541-L557 (this comment)
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L146-L154
🤖 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_real_traces.rs` around lines 541 -
557, Update crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L541-L557 to
remove the result.reducer_used skip for tool-bearing turns so the
majority-argument assertions execute; update is_reducer_call at
crates/mesh-mixture-of-agents/tests/sim_real_traces.rs#L146-L154 to recognize
the pack_for_actor system marker “## Advice from other models” and return
SYNTHESIZED for the actor call instead of replaying a worker body.

Comment thread evals/moa-openrouter/probe_tools.py
Comment on lines +31 to +41
text, tcs = oc.first_choice(resp)
ch = (resp.get("choices") or [{}])[0]
return {
"model": model,
"tier": oc.tier(model),
"elapsed": round(elapsed, 2),
"error": resp.get("error"),
"finish_reason": ch.get("finish_reason"),
"text": text,
"tool_calls": tcs,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist the complete worker response.

The recorder discards usage and all unselected OpenAI response fields. This conflicts with evals/moa-openrouter/README.md, which requires full response preservation, including usage. Store resp in each worker record, or add the omitted fields and narrow the documented contract.

Proposed fix
         return {
             "model": model,
             "tier": oc.tier(model),
             "elapsed": round(elapsed, 2),
+            "response": resp,
             "error": resp.get("error"),
             "finish_reason": ch.get("finish_reason"),
             "text": text,
             "tool_calls": tcs,
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text, tcs = oc.first_choice(resp)
ch = (resp.get("choices") or [{}])[0]
return {
"model": model,
"tier": oc.tier(model),
"elapsed": round(elapsed, 2),
"error": resp.get("error"),
"finish_reason": ch.get("finish_reason"),
"text": text,
"tool_calls": tcs,
}
text, tcs = oc.first_choice(resp)
ch = (resp.get("choices") or [{}])[0]
return {
"model": model,
"tier": oc.tier(model),
"elapsed": round(elapsed, 2),
"response": resp,
"error": resp.get("error"),
"finish_reason": ch.get("finish_reason"),
"text": text,
"tool_calls": tcs,
}
🤖 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 `@evals/moa-openrouter/record.py` around lines 31 - 41, Update the worker
record construction around the response handling to persist the complete OpenAI
response, including usage and unselected fields, by storing resp directly in
each record while retaining the existing derived fields as needed.

Comment thread evals/moa-openrouter/RESULTS.md

@i386 i386 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One functional issue found in the committee-cap path.

Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs Outdated

@i386 i386 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A second issue concerns the capacity signal that gates admission.

Comment thread crates/mesh-mixture-of-agents/src/worker.rs

i386 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Follow-up on the tiering signal: parsing 1B9B from a served name is brittle. Aliases/fine-tunes need not include a size, model names can include unrelated numbers, and the current fallback treats every unparseable name as big. That makes a destructive admission decision from an unverified label.

The better source is GGUF inspection: enumerate the GGUF tensors and sum their element_count to obtain the exact total stored-parameter count, then gossip that value through the existing ServedModelMetadata.parameter_count_b. Use it for MoA tiering when present; retain name parsing only as a lower-confidence fallback, and represent missing data as Unknown rather than big. For MoE we may eventually also want a distinct active-parameter signal, but total tensor count is already a materially better safety gate than the name heuristic.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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-llm-host-runtime/src/network/openai/moa_gateway/pool.rs`:
- Around line 242-244: Update assemble_worker_pool and
self_fill_from_extra_instances to retain and pass the resolved alias group and
required_tokens. Query all aliases in that group when self-filling, and apply
the same remote-host context eligibility check as add_worker_backend before
adding each endpoint, so insufficient-context hosts are excluded. Add
regressions covering equivalent canonical aliases and hosts below the required
context capacity.
- Around line 19-29: Update resolve_one_worker_from_aliases to accept and use
the existing WorkerBackendResolution value for immutable resolution inputs,
removing its separate targets, aliases, and required_tokens parameters as
appropriate. Remove the unused local_count parameter and eliminate the
#[allow(clippy::too_many_arguments)] suppression, updating all call sites to
match.
🪄 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: ba7e182b-4708-4854-986a-b213df4ea9cb

📥 Commits

Reviewing files that changed from the base of the PR and between 228a1ef and 365c470.

📒 Files selected for processing (3)
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs

Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 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-llm-host-runtime/src/network/openai/moa_gateway/pool.rs`:
- Around line 314-333: Ensure self-fill never duplicates an existing peer or
re-adds the local backend. Update add_worker_backend to return or store the
selected remote mesh::PeerId, collect peer identities already represented by the
current backends, and filter hosts_for_model(&name) in the self-fill loop before
creating each RemoteModelBackend; use the actual peer-id type from
RemoteModelBackend.

In `@evals/moa-openrouter/analyze_ablation.py`:
- Around line 175-196: Update the verdict logic in the analysis flow after the
B/C estimates to bootstrap a paired B−C differential using the same task
resamples, producing its own confidence interval. Base the “gain is CONTENT”
classification on that differential interval being entirely above zero, rather
than on b_lo; retain the existing B−A verdict and non-content-specific
comparison separately.

In `@evals/moa-openrouter/orclient.py`:
- Around line 74-76: Update the HTTPError handling in chat to decode the
response body defensively, falling back safely when the body is not valid UTF-8
so UnicodeDecodeError cannot escape the handler. Preserve the existing HTTP
status/detail formatting and structured error return behavior.

In `@evals/moa-openrouter/probe_tools.py`:
- Around line 27-31: Update each function schema in probe_tools.py, including
the schemas around the shown properties and the additional locations noted in
the review, to set additionalProperties to false alongside type, properties, and
required. Ensure every probe tool schema matches the production constraint
enforced by tool_guard.
- Line 79: Update the oc.chat call in the probe flow, including the
corresponding calls around the additionally affected lines, to pass
no_think=True. Preserve the existing model, messages, tools, token, and
temperature arguments so all workers and aggregators use the production MoA
thinking configuration.
- Line 199: Update the fanout_tool_result record call to store the complete
tool-result conversation by passing msgs_b as messages alongside the existing
results payload. Preserve the current record name and result data.

In `@evals/moa-openrouter/record_agentic.py`:
- Around line 131-140: Update consensus_tool to normalize valid JSON arguments
before counting tool-call proposals: parse each argument string, re-serialize
parsed objects with stable key ordering, and use that canonical form in the
consensus key. Preserve malformed arguments under a separate raw-string fallback
key, while keeping tool names and existing consensus selection behavior
unchanged.
🪄 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: 80edd6cb-badd-48c4-9245-c592678da89a

📥 Commits

Reviewing files that changed from the base of the PR and between 365c470 and 01d717d.

📒 Files selected for processing (40)
  • crates/mesh-llm-guardrails/src/rescue.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
  • crates/mesh-mixture-of-agents/src/arbiter.rs
  • crates/mesh-mixture-of-agents/src/backend.rs
  • crates/mesh-mixture-of-agents/src/context.rs
  • crates/mesh-mixture-of-agents/src/fanout.rs
  • crates/mesh-mixture-of-agents/src/lib.rs
  • crates/mesh-mixture-of-agents/src/normalize.rs
  • crates/mesh-mixture-of-agents/src/reducer.rs
  • crates/mesh-mixture-of-agents/src/refinement.rs
  • crates/mesh-mixture-of-agents/src/tool_guard.rs
  • crates/mesh-mixture-of-agents/src/tool_turn.rs
  • crates/mesh-mixture-of-agents/src/worker.rs
  • crates/mesh-mixture-of-agents/tests/eval_openrouter.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/ablation_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/committee_tasks.json
  • crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json
  • crates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rs
  • crates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rs
  • crates/mesh-mixture-of-agents/tests/sim_partial_worker_survival.rs
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs
  • crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs
  • crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rs
  • crates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
  • evals/moa-openrouter/README.md
  • evals/moa-openrouter/RESULTS.md
  • evals/moa-openrouter/agentic.jsonl
  • evals/moa-openrouter/analyze_ablation.py
  • evals/moa-openrouter/corpus.jsonl
  • evals/moa-openrouter/fanout.jsonl
  • evals/moa-openrouter/make_fixture.py
  • evals/moa-openrouter/orclient.py
  • evals/moa-openrouter/probe_tools.py
  • evals/moa-openrouter/record.py
  • evals/moa-openrouter/record_agentic.py
🚧 Files skipped from review as they are similar to previous changes (31)
  • crates/mesh-mixture-of-agents/tests/sim_strong_patience.rs
  • crates/mesh-mixture-of-agents/src/tool_guard.rs
  • crates/mesh-mixture-of-agents/tests/sim_tool_call_text_not_passed_as_content.rs
  • crates/mesh-mixture-of-agents/src/reducer.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/mod.rs
  • evals/moa-openrouter/fanout.jsonl
  • crates/mesh-mixture-of-agents/tests/sim_tool_result_routes_to_reducer.rs
  • crates/mesh-mixture-of-agents/tests/sim_all_workers_fail.rs
  • crates/mesh-mixture-of-agents/src/normalize.rs
  • crates/mesh-mixture-of-agents/tests/sim_partial_worker_survival.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/committee_tasks.json
  • crates/mesh-mixture-of-agents/tests/sim_enable_thinking_propagation.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/ablation_tasks.json
  • crates/mesh-mixture-of-agents/src/worker.rs
  • crates/mesh-mixture-of-agents/src/backend.rs
  • crates/mesh-mixture-of-agents/tests/sim_worker_accounting.rs
  • crates/mesh-llm-host-runtime/src/network/openai/ingress.rs
  • crates/mesh-mixture-of-agents/tests/fixtures/real_traces.json
  • evals/moa-openrouter/agentic.jsonl
  • crates/mesh-mixture-of-agents/tests/sim_real_traces.rs
  • crates/mesh-mixture-of-agents/src/refinement.rs
  • evals/moa-openrouter/corpus.jsonl
  • crates/mesh-mixture-of-agents/tests/sim_refinement_mesh_conditions.rs
  • crates/mesh-mixture-of-agents/src/tool_turn.rs
  • crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/workers.rs
  • crates/mesh-mixture-of-agents/src/context.rs
  • crates/mesh-llm-guardrails/src/rescue.rs
  • crates/mesh-mixture-of-agents/tests/eval_openrouter.rs
  • crates/mesh-mixture-of-agents/src/lib.rs
  • crates/mesh-mixture-of-agents/src/fanout.rs
  • crates/mesh-mixture-of-agents/src/arbiter.rs

Comment thread crates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs
Comment on lines +175 to +196
b_pt, b_k = point_estimate(tasks, "B")
c_pt, c_k = point_estimate(tasks, "C")
b_lo, b_hi = bootstrap_ci(tasks, "B", args.iters, args.seed)
c_lo, c_hi = bootstrap_ci(tasks, "C", args.iters, args.seed)

print(" net uplift = P(rescue) - P(harm), equal-weight mean over tasks")
print(f" B (real) uplift {b_pt:+.3f} 95% CI [{b_lo:+.3f}, {b_hi:+.3f}] (tasks n={b_k})")
print(f" C (shuffled) uplift {c_pt:+.3f} 95% CI [{c_lo:+.3f}, {c_hi:+.3f}] (tasks n={c_k})")
print(f" differential B-C: {b_pt - c_pt:+.3f} (content effect beyond token/prompt effect)")
print()

# Verdicts (directional; the CI is what matters for a claim).
if b_lo > 0:
print(" => references HELP: B net uplift CI is entirely > 0")
elif b_hi < 0:
print(" => references HARM: B net uplift CI is entirely < 0")
else:
print(" => inconclusive: B net uplift CI spans 0")
if b_pt - c_pt > 0 and b_lo > 0:
print(" => and the gain is CONTENT (B > C), not just extra tokens/prompt")
elif abs(b_pt - c_pt) < 0.02:
print(" => gain (if any) is NOT content-specific (B ~ C)")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bootstrap the B−C differential before classifying a content effect.

Line 193 tests b_lo > 0, but b_lo is the lower confidence bound for B−A. It is not a confidence bound for B−C. This condition can report a content-specific gain when B−C is statistically uncertain.

Use the same paired task and draw resamples to calculate a B−C confidence interval. Base the content verdict on that interval.

🤖 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 `@evals/moa-openrouter/analyze_ablation.py` around lines 175 - 196, Update the
verdict logic in the analysis flow after the B/C estimates to bootstrap a paired
B−C differential using the same task resamples, producing its own confidence
interval. Base the “gain is CONTENT” classification on that differential
interval being entirely above zero, rather than on b_lo; retain the existing B−A
verdict and non-content-specific comparison separately.

Comment thread evals/moa-openrouter/orclient.py
Comment thread evals/moa-openrouter/probe_tools.py
"""

def one(model):
resp, elapsed = oc.chat(model, messages, tools=tools, max_tokens=max_tokens, temperature=0.8)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the production MoA thinking setting.

oc.chat defaults no_think to False. Its contract states that no_think=True mirrors the MoA default. These calls can therefore measure reasoning-enabled workers and aggregators instead of the production configuration. Pass no_think=True; orclient.chat already retries without the flag for providers that require reasoning.

Proposed fix
-        resp, elapsed = oc.chat(model, messages, tools=tools, max_tokens=max_tokens, temperature=0.8)
+        resp, elapsed = oc.chat(
+            model, messages, tools=tools, max_tokens=max_tokens, temperature=0.8, no_think=True
+        )
...
-        aggregator, msgs, tools=TOOLS if with_tools else None, max_tokens=384, temperature=0.3
+        aggregator, msgs, tools=TOOLS if with_tools else None,
+        max_tokens=384, temperature=0.3, no_think=True

Also applies to: 135-137

🤖 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 `@evals/moa-openrouter/probe_tools.py` at line 79, Update the oc.chat call in
the probe flow, including the corresponding calls around the additionally
affected lines, to pass no_think=True. Preserve the existing model, messages,
tools, token, and temperature arguments so all workers and aggregators use the
production MoA thinking configuration.

},
]
res_b = summarize(fan_out(msgs_b, TOOLS), "B. TOOL-RESULT TURN (agentic step 2)")
record("fanout_tool_result", results=res_b)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Record the tool-result input conversation.

This record contains only results. It omits the user prompt, assistant tool call, tool-call ID, and tool result in msgs_b. A replay corpus cannot reconstruct this tool-result turn. Store messages=msgs_b with the result.

Proposed fix
-    record("fanout_tool_result", results=res_b)
+    record("fanout_tool_result", messages=msgs_b, results=res_b)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
record("fanout_tool_result", results=res_b)
record("fanout_tool_result", messages=msgs_b, results=res_b)
🤖 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 `@evals/moa-openrouter/probe_tools.py` at line 199, Update the
fanout_tool_result record call to store the complete tool-result conversation by
passing msgs_b as messages alongside the existing results payload. Preserve the
current record name and result data.

Comment on lines +131 to +140
def consensus_tool(workers):
"""Most-proposed (name, arguments) across workers, or None."""
counts = {}
for w in workers:
for c in w.get("tool_calls") or []:
key = (c["function"]["name"], c["function"].get("arguments") or "{}")
counts[key] = counts.get(key, 0) + 1
if not counts:
return None
return max(counts.items(), key=lambda kv: kv[1])[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Normalize JSON arguments before counting tool-call consensus.

Equivalent argument objects can use different key order or whitespace. The current raw-string key splits those votes. A lower-support proposal can then control the canned observation and all later recorded steps.

Parse valid argument JSON and serialize it with stable key ordering before counting proposals. Keep malformed arguments in a separate raw fallback key.

🤖 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 `@evals/moa-openrouter/record_agentic.py` around lines 131 - 140, Update
consensus_tool to normalize valid JSON arguments before counting tool-call
proposals: parse each argument string, re-serialize parsed objects with stable
key ordering, and use that canonical form in the consensus key. Preserve
malformed arguments under a separate raw-string fallback key, while keeping tool
names and existing consensus selection behavior unchanged.

…laky peers

Agentic MoA turns now reliably produce tool calls, and diverging answers are
synthesized instead of relaying one arbitrarily-chosen worker's text.

Fixes found by replaying recorded traces from 9 open-weight models:

* Tools were withheld from workers. `query_uses_tools` was derived from
  `looks_like_tool_intent`, an English keyword match on the user's text. "The
  test suite is failing. Find out which test fails and why" matches nothing, so
  workers were dispatched without tool schemas and the arbiter ran with
  has_tools=false — a unanimous tool proposal then fell through to the answer
  path and leaked "calling search" as prose. 5 of 10 recorded scenarios hit
  this. Tool availability is now the caller's declaration.

* Argument outliers could win a unanimous tool call. Native tool calls are all
  normalized to a fixed 0.9 confidence, so the confidence-only tiebreak could
  not separate 8 workers proposing {"path":"src"} from one hallucinating
  {"path":"rust_project/src"}. Added argument clustering (key-order
  independent); the largest cluster wins and argument-free calls never outvote
  filled-in ones.

* Early exit committed on tied arguments. Early exit runs on whoever has
  arrived, so two fast models agreeing on a tool name with different arguments
  was a coin flip that also aborted the workers who would have broken the tie.
  It now waits.

* Truncation was invisible. `finish_reason` was never read; 39/140 recorded
  responses came back "length" and 24 carried partial text, which parsed as a
  normal answer and could be returned verbatim. Plumbed through as
  BackendReply.truncated -> WorkerOutput.truncated; such answers are barred
  from consensus and verbatim output but still feed synthesis, labelled
  incomplete.

* A strict endpoint could kill a worker permanently. minimax-m2.5 returns
  HTTP 400 "Reasoning is mandatory for this endpoint" and failed 12/12
  requests; HttpBackend now drops the thinking flags and retries once.

Policy changes:

* Thinking is always off for MoA workers, not merely defaulted off. The
  previous escape hatch only let callers request the broken configuration:
  qwen3-32b spent 408 reasoning tokens against a 384-token cap and returned
  null content. Ignored overrides are logged.

* Diverging answers go to the reducer. Returning the top-confidence answer was
  near-arbitrary because models rarely emit our confidence envelope, so
  everything defaults to 0.5 and max_by returns whichever worker finished
  first. Agreement still short-circuits without paying for synthesis.

* Reducer prompt adopts Together's aggregator framing (synthesize, critically
  evaluate, agreement is not proof), keeping our per-worker attribution,
  structured tool proposals, and 500-char payload bound.

Tests: new sim_real_traces.rs replays 56 recorded cases from 9 models through
handle_turn, plus regression unit tests for each bug. Recorders and corpora
live in evals/moa-openrouter/.
…r acts

Tool-bearing turns now use the Hermes/Nous-style asymmetric shape instead of a
majority vote across workers:

- references run TOOL-FREE and only advise in prose
- the single best tool-caller (the "actor") acts on that advice with the real
  tools and emits the tool call

Tool authority now tracks capability, not popularity. The old path let several
weak models proposing a popular-but-wrong tool outvote the one strong
tool-caller that picked correctly (observed: qwen3-32b alone chose run_command
for a failing-test triage while the smaller models chose list_dir, and the vote
shipped list_dir). An actor model removes that failure class instead of patching
the vote arithmetic.

Stays a pure stateless /v1/chat/completions turn — references are regenerated
from the caller's transcript each request, and the external client still owns
tool execution. Text-only turns are unchanged (symmetric fan-out +
synthesis-on-divergence).

Actor selection is capability-first: the host ranks candidates by gossiped
`tool_use` level (Supported > Likely > None), then model size tier, then stable
order, and passes the ordering to the engine via the new
`GatewayConfig.actor_candidates`. Empty ordering falls back to the engine's
name-derived size tier, so existing callers and tests are unaffected.

Mesh guardrails for mixed/public meshes: references are gathered with a bounded
wait (proceed at a majority of advisors, never block on the slow tail), and the
actor is called through the existing hedged ladder so a slow/broken best
candidate falls through to the next tool-capable peer.

New:
- crates/mesh-mixture-of-agents/src/tool_turn.rs — asymmetric tool-turn handler
- context::pack_for_actor — "advise, then act" framing (vs synthesis framing)
- fanout::gather_references — bounded, tool-free reference collection
- GatewayConfig.actor_candidates + reducer_candidates honours it
- host compute_actor_candidates from gossiped tool_use + size

The dead majority-vote code in the arbiter (now unreachable — tool turns bypass
it) is removed in the follow-up commit.

Tests: eval_openrouter.rs adds a live OpenRouter harness (ignored by default)
that runs the real handle_turn over 6 open-weight models with mesh-realism
latency/failure injection, for benchmarking asymmetric MoA vs single models.
192 engine tests pass; clippy -D warnings and fmt clean on both crates.
Tool turns now take the asymmetric actor path (previous commit), so the
arbiter's tool-proposal voting is unreachable — and it *was* the
majority-of-weakness bug: a popular-but-wrong tool choice from several weak
models could outvote the one strong tool-caller that picked correctly.

Removes it rather than leaving dead code:

- delete best_tool_proposal, best_tool_proposal_by_consensus, and
  decisive_argument_cluster
- drop the tool-arbitration and tool-vs-answer branches from arbitrate, and
  the tool-consensus branch from try_early_decision
- drop the now-unused has_tools parameter from arbitrate, try_early_decision,
  single_output_decision, and gather_workers_incremental
- the arbiter is now purely an answer/critique/uncertainty arbiter; tool-shaped
  text on the text path is already demoted to Uncertainty by
  enforce_tool_call_contract (tools disabled ⇒ empty allow-list)
- drop the arbiter tests that pinned the removed tool-vote behavior; keep and
  re-point every answer-consensus, truncation, synthesis, and tier-gate test

No behavior change for text turns. 178 engine tests pass; host-runtime lib
tests (1851) pass; clippy -D warnings and fmt clean on
mesh-mixture-of-agents, mesh-llm-host-runtime, and mesh-llm.
Replaces the confounded "MoA vs best single model" comparison with a
within-actor ablation that isolates the one variable that matters: the
references. One pinned actor, identical sampling / token budget / system
prompt across all arms; only the advice changes:

  A. actor alone (no references)
  B. actor + real references (the production tool path)
  C. actor + shuffled references (advice from a different task, length-similar)

Metric: rescue (A✗→B✓) minus harm (A✓→B✗). Arm C is the key control — it
separates "useful information" from "extra tokens + a think-carefully prompt".

Why the change: the earlier best-single comparison ran solo models at different
sampling (0.8/512) than the actor (0.3/2048) and with a different system
prompt, and scored transient 429/502/504 and tool-unsupported endpoints as
capability failures. Those numbers measured sampling+prompt+infra, not the
design, and are not citable. This ablation removes all of that by construction:
the same scorer hits all three arms of the same actor, so an imperfect label
largely cancels in the rescue-minus-harm delta.

Also:
- chat_completion_retrying: predeclared retry on transient infra errors, which
  are then excluded (∅) from the capability analysis rather than scored as
  wrong answers.
- MOA_ABLATION_ACTOR / MOA_ABLATION_DRAWS env overrides.
- the old best-single test is kept but documented as a confounded smoke test,
  not evidence.

Pilot findings (5 draws, 4 tasks, ignored/live):
- Strong actor (qwen3-32b): 20/20 all arms — aces every task alone, so
  references are inert-but-harmless (no headroom to measure rescue).
- Weak actor (qwen3-8b): A 15/20, B 20/20, C 15/20 — net uplift +5, all on the
  one task with headroom (triage). Real advice rescued the weak actor every
  draw; SHUFFLED advice did not (C=A), so the gain is advice CONTENT, not token
  count or the decision prompt. Zero harm.

Reading: reference value tracks actor-alone headroom — a safety net when the
actor is weak, dead weight (not damage) when strong. Suggests gating the
reference phase on actor strength. This is a directional pilot, not the
merge-blocking study (that needs ~40 preregistered stratified tasks, k>=10
draws, a paired hierarchical bootstrap CI, and the production-selected actor).
…yzer

The defensible version of the pilot, committed BEFORE running so the labels
are preregistered and can't be seen to chase results.

- tests/fixtures/ablation_tasks.json: 40 preregistered tasks, 4 strata x 10
  (inspect / search / execute / no_tool). Set-valued accept labels
  (accept_tools is a SET; empty = "no tool call"), with optional arg substring
  constraints — addresses the brittle single-tool label from the pilot.
- ablation_scaled_study: loads the fixture, runs A/B/C arms at k draws
  (default 10) with bounded concurrency, writes one JSONL trial per
  (draw,task,arm) to MOA_ABLATION_OUT. Live measurement only; no stats here.
- evals/moa-openrouter/analyze_ablation.py: deterministic paired HIERARCHICAL
  bootstrap — resample tasks within stratum, then draws within task — for a
  95% CI on net uplift = P(rescue) - P(harm), plus the shuffled-arm control
  (content effect = B_uplift - C_uplift). Validated on synthetic data: 3/40
  rescue tasks -> +0.075 point estimate, shuffled +0.000, and the CI honestly
  spans 0 when rescue is sparse (no over-claiming).

Arms (one pinned actor, identical sampling/prompt; only references vary):
  A actor alone · B actor + real references · C actor + shuffled references.

Env: MOA_ABLATION_ACTOR, MOA_ABLATION_DRAWS, MOA_ABLATION_CONCURRENCY,
MOA_ABLATION_OUT. Ignored by default (live network + cost).
Cut the essay-length doc/inline comments added with the actor design down to the
non-obvious "why". No code change.

- tool_turn.rs: 26-line module preamble -> 6 lines; inline comments tightened
  (20% -> 10% comment density)
- context.rs: pack_for_actor docstring 16 -> 5 lines; reducer synthesis-framing
  block 14 -> 6 lines
- workers.rs: compute_actor_candidates docstring 21 -> 6 lines (kept the terse
  sort-key comments, which are load-bearing)
Two more ablation arms to answer "can peers help tool selection at all?",
reusing the A/B/C JSONL schema + analyze_ablation.py.

matched_peer_structured_study — do similar-strength, different-family peers
help a fixed finalizer via STRUCTURED candidate tool calls (not prose)?
  A solo · B diverse (2 different-family peers) · C homogeneous (2 resamples
  of the finalizer's own model). B-C isolates cross-family diversity from
  extra sampling; also records oracle-union.

correction_rescues_weak_tool_caller — the mesh scenario neither Hermes nor
Together handles: a weak tool-caller with no strong peer. Tests correction of
the CONCRETE drafted call instead of pre-hoc advice.
  A draft-alone · B deterministic (schema-validate + re-prompt on structural
  failure) · C semantic (different-family critic reviews the concrete call,
  finalizer revises once).

Findings (live, directional): every tool-selection intervention was
inert-to-harmful vs routing to a capable model. Pre-hoc structured proposals
were flat (37-39/40 all arms). Deterministic correction fired ~never
(qwen3-8b already drafts structurally valid calls ~95%); the residual failures
are semantic (wrong tool choice), which neither validation nor a strong critic
fixed because the revision still runs through the weak actor. Conclusion: tool
selection is a "best capable model acts" task; MoA's value is on the
answer/chat path (untested).
Tests where MoA's value should live per Together's validated claim: open-ended
answer QUALITY on realistic agent-session turns (reason-over-tool-output,
planning, explanation) — not tool selection.

Fixed aggregator; only its input varies:
  A alone · B committee (synthesize 3 diverse-peer drafts, 1 round)
  C layered (peers refine seeing each other first, then synthesize — Together's
  `layers`)

Judged pairwise by an out-of-pool different-family judge (gpt-4o-mini),
position-swapped (win only if consistent both orders), output lengths logged.

- tests/fixtures/committee_tasks.json: 15 realistic reasoning/answer turns
- committee_beats_solo_on_reasoning: writes per-trial JSONL

Pilot findings (2 draws, live; DIRECTIONAL, underpowered):
- committee(B) vs solo(A): win 6 / tie 2 / loss 2 — positive but sign-test
  p=0.29, NOT significant at n=10. Every B win was also the longer answer, so
  length is not cleanly ruled out.
- layered(C) vs committee(B): loss 6 / tie 2 / win 2 — Together's extra
  refinement round is negative value on these tasks, at extra cost.
- 20/30 trials skipped because the aggregator (qwen3-32b) returned empty
  content (reasoning-budget exhaustion — the content:null bug Hermes'
  troubleshooting.md documents). A flaky aggregator degrades the committee;
  the instrument needs empty-output handling before a real run.

Net: first directionally-positive result for MoA in this investigation, but
unproven. Contrast with 5 tool-selection experiments that were all
null-to-harmful. Supports the task-split: route tools to one caller, convene a
(single-round) committee on reasoning turns.
…l transcript

Our references were packed very differently from Hermes', and it was measurably
costly. Adds `pack_for_reference`, which gives advisors only the conversation's
user/assistant prose:

- strips the agent system prompt (an advisor told "you are a coding agent, run
  the tests" role-plays the actor instead of advising it)
- strips the tool transcript (prior tool_calls + results anchored every advisor
  on the trajectory already taken, collapsing the error-independence that makes
  aggregation worth anything)
- drops the "respond with your best answer or tool call" instruction: advisors
  hold no schemas, so asking for a tool call yields tool-shaped prose — exactly
  the advice that pulled the actor off its own better choice
- uniform view across advisors (no per-role trimming), so the packing is a
  stable function of history and caches across iterations
- caps advisor output at 600 tokens (advisor generation dominates turn latency;
  the turn waits for the slowest advisor)

Head-to-head on the same preregistered study (strong actor qwen3-32b, 40 tasks
x 10 draws, identical everything except packing):

  packing      B pass    net uplift      95% CI
  original     359/400   -0.102          [-0.170, -0.045]
  hermes       385/400   -0.037          [-0.090, -0.003]

Harm cut by ~64%. Per-category: search -14 -> -2, execute -25 -> -13,
inspect -2 -> 0. The content-specific component (B-C differential) fell from
-0.075 to -0.015, i.e. with correct packing the residual harm is no longer
mostly "bad advice content".

Honest read: most of the harm I previously attributed to "references" was an
artifact of how WE packed them, not a property of reference-based MoA. The
direction still stands though — even correctly packed, references remain a
small but statistically real regression for a STRONG actor on tool selection
(CI still entirely below zero).

Adds 4 unit tests pinning the packing contract (no tool-call request, no system
prompt leak, no tool-transcript leak, prose preserved), and
MOA_REFERENCE_PACKING=hermes to select the style in the eval harness.
…t failed

Writes up every live study in one place (evals/moa-openrouter/RESULTS.md) so
the conclusions and their caveats survive the investigation.

Headline 2x2 (40 preregistered tasks x 10 draws, paired hierarchical bootstrap):

  actor    packing    B pass     net uplift   95% CI
  strong   original   359/400    -0.102       [-0.170, -0.045]  <- the bug
  strong   hermes     385/400    -0.037       [-0.090, -0.003]
  weak     original   365/400    -0.013       [-0.090, +0.070]
  weak     hermes     377/400    +0.017       [-0.053, +0.100]

Two monotonic effects: fixing the packing helps in both actor conditions, and
references are worth more to a weaker actor. The only significant cell is the
original-packing strong-actor harm — i.e. our bug, not a property of MoA.

Per-stratum (weak + hermes) shows references help exactly where the actor has
headroom (search +10, execute +4) and hurt where it was already perfect
(inspect -7). That is a gating signal.

Also records what did NOT work for tool selection (pre-hoc structured
proposals: flat; deterministic correction: never fires, the weak actor already
emits valid calls ~95%; semantic correction: negative, since the revision still
runs through the weak actor), and the committee pilot on reasoning turns
(directionally positive, n=10, not significant; Together's layering loses to
single-round).
The Hermes-style packing was measured and unit-tested but only wired into the
eval harness — the production tool path still used pack_for_worker_selected,
the exact packing measured at -0.102 net uplift.

tool_turn now calls pack_for_reference: conversation prose only, no agent
system prompt, no tool transcript, no request for a tool call. Same head-to-head
(strong actor, 40 tasks x 10 draws) puts this at -0.037 vs -0.102, and it is the
only configuration where references show a positive point estimate for a weak
actor (+0.017). See evals/moa-openrouter/RESULTS.md.
@michaelneale

Copy link
Copy Markdown
Collaborator Author

@i386 thanks - have it reading real data now

@michaelneale

Copy link
Copy Markdown
Collaborator Author

@i386 — fixed the tiering P1s you flagged (name-based tier / unparseable-name-as-big driving destructive admission). Now exactly your prescription: authoritative size from GGUF tensor sum, gossiped via the existing parameter_count_b, used for MoA tiering with name-parse only as a lower-confidence fallback and missing = Unknown (never big, never filtered).

Commit: 3b94cf0d (rebased/merged forward since). Three parts:

1. Producer computes the real sizecrates/model-artifact/src/gguf.rs, scan_gguf_total_parameters: sums every tensor's element count from the header/tensor-info table (no tensor data read):

pub fn scan_gguf_total_parameters(path: &Path) -> Option<u64> {
    let GgufHeader { file: mut f, n_tensors, n_kv } = open_gguf_header(path)?;
    skip_all_kv_pairs(&mut f, n_kv)?;
    let mut total: u64 = 0;
    for _ in 0..n_tensors {
        let _name = read_gguf_string(&mut f).ok()?;
        let n_dims = read_u32(&mut f).ok()?;
        if n_dims > MAX_GGUF_TENSOR_DIMS { return None; }
        let mut elements: u64 = 1;
        for _ in 0..n_dims {
            let dim = read_u64(&mut f).ok()?;
            elements = elements.checked_mul(dim)?;
        }
        let _ggml_type = read_u32(&mut f).ok()?;
        let _offset = read_u64(&mut f).ok()?;
        total = total.checked_add(elements)?;
    }
    Some(total)
}

2. Producer prefers it, falls back to namecrates/mesh-llm-host-runtime/src/models/profile.rs: parameter_count_b = tensor sum (÷1e9) when the local GGUF is present, else parameter_count_b_from_text(name), else None. Gossiped via ServedModelMetadata.parameter_count_b.

3. Consumer tiers off the gossiped valuecrates/mesh-llm-host-runtime/src/network/openai/moa_gateway/pool.rs. The MoA orchestrator has no peer GGUFs, so it reads gossiped parameter_count_b only:

enum SizeTier { Small, Big, Unknown }

fn tier_for(name: &str, sizes: &HashMap<String, f64>) -> SizeTier {
    if let Some(b) = sizes.get(&canonical_base_name(name)) {
        return if *b < SMALL_TIER_MAX_B { SizeTier::Small } else { SizeTier::Big };
    }
    match mesh_llm_guardrails::model_param_size_b(name) {   // name-parse fallback
        Some(b) if (b as f64) < SMALL_TIER_MAX_B => SizeTier::Small,
        Some(_) => SizeTier::Big,
        None => SizeTier::Unknown,                          // missing = Unknown, not big
    }
}

Admission excludes only verified Small and counts only verified Big toward the ">=2 big remain" gate — Unknown is never strong and never filtered. cap_committee also now ranks by this size, not the tool-actor ranking (your other P1 — a tool_use=None big model was getting evicted for tool-advertising small ones on answer turns).

Verified live on a 2-node mesh (MacBook 3B + mini Qwen3.5-4B): gossiped sizes went from a bogus name-parsed 4910 to real 3.21 / 4.21, and admission now no-ops on unknown-size workers. Two new unit tests: admission_never_filters_unknown_size_worker, admission_falls_back_to_name_parse_without_gossip.

Note deliberately deferred to follow-up: MoE active-parameter signal (total tensor count is the safety gate for now, as you said). Does this match what you had in mind?

@i386

i386 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Producer prefers it, falls back to namecrates/mesh-llm-host-runtime/src/models/profile.rs: parameter_count_b = tensor sum (÷1e9) when the local GGUF is present, else parameter_count_b_from_text(name), else None. Gossiped via ServedModelMetadata.parameter_count_b.

I think the fallback to name should be removed. if you cant get this info from gguf, the model must be ranked as the lowest param model.

As would be worth chucking anything in your cache at this and checking if that tensor counts matches the name to validate.

michaelneale and others added 2 commits August 5, 2026 14:24
Per i386: parsing NNb from a served name is brittle and a destructive
admission decision must not rest on an unverified label. Removed the name
fallback entirely.

Producer (profile.rs): parameter_count_b comes ONLY from the GGUF tensor sum.
No local GGUF to sum ⇒ None (no guessed count). Deleted the now-dead
parameter_count_b_from_text + its test.

Consumer (pool.rs): tier_for uses only the gossiped verified size. Dropped
SizeTier::Unknown — a model with no verified size is Small (weakest), so an
unverifiable label can never masquerade as big and displace a real strong
worker. cap_committee ranking updated (no-size ranks last). Admission excludes
only verified-small; with no verified big there is nothing to protect so an
all-unsized pool is untouched.

Tests updated: unsized-worker-is-weakest (excluded next to verified big),
no-verified-sizes keeps all. 71 moa_gateway + 28 profile tests pass; clippy -D
warnings and fmt clean.
@ndizazzo

ndizazzo commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

MeshLLM PR 1116 Homelab Validation Report

Field Value
Candidate commit ed90d75e156fbd6ea896f9a9edee5e427017deb6
Pull request Mesh-LLM/mesh-llm#1116, “MoA: ensembling that improves as mesh nodes join”
Base / current main PR base 70d4f3338c5020cdc9e05d1863a6b3787b5bed3c; tested current origin/main b7c26ebe789ec72a0a58db85d20b7c4a04e5832d
Previous/latest GitHub release v0.74.0, published 2026-07-27
Validation run ID 20260805T043845Z-ed90d75
Started / completed (UTC) 2026-08-05 04:38 / 2026-08-05 05:26
Decision NOT_READY for the claimed quality improvement; live MoA functionality PASS WITH DEFECTS

General Findings

PR 1116's MoA machinery works through the shipped OpenAI HTTP path on a real
three-node private homelab mesh. Fan-out, same-model self-fill, refinement,
reduction, streaming, single-actor tool calls, tool-result continuation, bounded
peer loss, and rejoin were all observed. All three final nodes report the same
mesh ID, two peers, a ready CUDA runtime, and no severe log events.

The PR is nevertheless NOT_READY for its headline claim that the shipped
mesh model improves as nodes join
. The committed results document explicitly
reports the shipped moa::handle_turn path at 9W/59T/12L (p = 0.66) versus the
best pool member and calls the unresolved harness-versus-production gap
merge-blocking. The stronger 49W/24T/6L, 48W/23T/2L, and 11W/68T/1L results
cannot be independently recomputed from committed trial outputs; the repo
contains corpora, small fixtures, scripts, and narrative results, but not the
scaled per-trial judge outputs. Re-running the paid OpenRouter studies and a
second judge was outside this homelab run.

Two additional release defects were found: repository just check-release
fails its Linux/aarch64 CUDA asset-parity assertion, and the chat endpoint accepts
missing or incorrectly typed messages and fabricates a 200 response. These do
not invalidate the live MoA observations, but they reinforce the NOT_READY
decision (unrelated).

Scope And Exclusions

Validated:

  • Fresh detached PR worktrees and native builds on mesh1, mesh2, and
    carrack.
  • CUDA 13.2 / SM87 on two Jetson AGX Orin 64 GB systems; CUDA 13.3 / SM120+SM86
    on RTX 5090 + RTX 3080.
  • Extracted product bundles, host import policy, private QUIC mesh, management
    status, /v1/models, chat completions, SSE, tool calls, tool results, negative
    requests, peer shutdown/rejoin, and final log scan.
  • Same-model topology using unsloth/Qwen3-0.6B-GGUF:Q4_K_M on all nodes.
  • Local Rust tests, formatting, and Clippy for MoA, guardrails, host runtime, and
    shipped binary.

Canonical Change Inventory And Validation Ledger

ID Category Release claim Planned checks Status Evidence / defects
MOA-1 FEATURE Answer turns draft, optionally refine, and aggregate Live non-stream and divergent answer request PASS 200 response; fanout, 4 successful proposal-stage calls (2 drafts + 2 refinements), reducer used once
MOA-2 FEATURE Same-model meshes self-fill from distinct nodes, capped at two active participants Three nodes, one identical model; inspect early-exit and layered accounting PASS Early-exit: 2/2 worker calls; divergent turn: 4/4 calls across draft+refine; reducer true
MOA-3 FEATURE Tool turns route to a single best actor Required tool request and continuation PASS Tool call had 1/1 worker, valid get_weather({city:"Toronto"}); continuation labeled tool-result
MOA-4 FEATURE Streaming remains OpenAI-compatible SSE request PASS 200 event stream, content chunk, terminal stop chunk, [DONE]
MOA-5 FEATURE Slow/absent peers do not stall turns Stop carrack, request through mesh1, rejoin carrack PASS Request returned in 949 ms with 2/2 successful calls; carrack rejoined same mesh
MOA-6 REVISION Weak worker admission control Unit/integration suite and source review PARTIAL Focused suite passes; not reproduced live with two distinct verified big models plus a weak model
MOA-7 BUG_FIX UTF-8 guardrail panic fixed Guardrails suite and final log scan PASS 15 guardrail tests pass; no panic/fatal/error in final logs
MOA-8 RESULT Shipped MoA improves over best member Review committed study and live functional evidence FAIL Own RESULTS.md says shipped path is parity: 9W/59T/12L, p=0.66, with unexplained orchestration gap
API-1 BUG_FIX Production OpenAI path remains contract-safe Valid, invalid JSON, missing/wrong messages, unknown model FAIL Invalid JSON 400 and unknown model 404; missing/wrong messages incorrectly return 200 with fabricated prose

Environment And Build Provenance

All worktrees were created at ~/dev/mesh/mesh-llm-pr1116, detached at the
exact PR SHA, then built with repository just recipes. dist/native-runtimes/
is untracked build output; source files remained unchanged.

Host alias OS / arch Hardware / backend Canonical build commands Product archive SHA-256 Manifest / import policy Status
mesh1.patio51.com Ubuntu 24.04 / aarch64 AGX Orin 64 GB, CUDA 13.2, SM87 just release-host-build; MESH_LLM_CUDA_TOOLKIT_MAJOR=13 LLAMA_STAGE_CUDA_ARCHITECTURES=87 just release-runtime-build cuda; just release-bundle-aarch64-cuda ... c7a80b3a66c32260533ed709f1a921928f14d94e0f3ea7cd43dc9ce564d033ca ABI 0.1.35, CUDA 13; zero rejected host imports PASS
mesh2.patio51.com Ubuntu 24.04 / aarch64 AGX Orin 64 GB, CUDA 13.2, SM87 same as mesh1 2f3941be95d91d9fcc10bdef7b7495e5618d95231baa4a67eff3372ff4973132 ABI 0.1.35, CUDA 13; zero rejected host imports PASS
carrack.patio51.com Arch Linux / x86_64 RTX 5090 + RTX 3080, CUDA 13.3, SM120+SM86 just release-host-build; MESH_LLM_CUDA_TOOLKIT_MAJOR=13 LLAMA_STAGE_CUDA_ARCHITECTURES='120;86' just release-runtime-build cuda; just release-bundle-cuda ... d1cc74704b88b5c837cd7c7dab481024efa4aec587ea5ee6fca3a084c6b9dd31 ABI 0.1.35, CUDA 13; zero rejected host imports PASS

patchelf 0.19.1 was installed on carrack because the release packaging script
requires it. The generic release-runtime-build cuda defaults its manifest to
CUDA 12 even when compiling with CUDA 13, so the explicit
MESH_LLM_CUDA_TOOLKIT_MAJOR=13 setting was required for runtime compatibility.

Product Validation Results

Area Case Expected Observed Status
Packaging Execute extracted products Host selects adjacent fresh CUDA 13 runtime All three selected ABI 0.1.35 runtime and loaded model PASS
Startup Fresh candidate startup API, console, model ready All hosts ready; final severe event count 0 PASS
Private mesh Three candidate nodes Same mesh ID, 2 peers each, private 380326a05b75a239f9f2ab2911d583b7, 2 peers each, private PASS
OpenAI API Same-model exact answer MoA observability and usable content early-exit, 2/2, 508 ms, MOA_OK. PASS
Inference Divergent reasoning answer Layered path and reducer fanout, 4/4 stage calls, reducer attempt 1, 2874 ms PASS
Streaming Chat SSE Valid chunks and [DONE] 3 data lines, expected content, [DONE] PASS
Tool calls Required call One actor and valid JSON arguments 1/1 worker, valid Toronto call PASS
Tool results Continue after result Final prose response tool-result, 1/1, 705 ms PASS
Reliability 3 loops each for auto and mesh Tool-call/result parity, streaming and non-streaming mesh 12/12; auto 10/12 PARTIAL
Nightly smoke chat, stream, tools on auto,mesh Stable success All configured functional steps passed; attestation unconfigured PASS
Negative API Invalid JSON / unknown model 4xx 400 / 404 PASS
Schema API Missing/wrong messages 400 200 with unrelated generated answer FAIL
Failure / recovery Remove and rejoin carrack Remaining mesh serves; same mesh restored 949 ms request succeeds; carrack rejoins with 2 peers PASS
Logs Severe event scan No panic/fatal/error mesh1 438 JSON lines, mesh2 316, carrack 284; 0 severe PASS
Release lane just check-release Clean aarch64 CUDA asset parity mismatch FAIL

Private Mesh Evidence

Final topology (left running):

Node API / console / QUIC Node ID Peers Model
mesh1 19337 / 13131 / 17842 00c6868b1b 2 unsloth/Qwen3-0.6B-GGUF:Q4_K_M
mesh2 19447 / 13145 / 17843 520112d875 2 same
carrack 19557 / 13155 / 17844 c45f63ec2e 2 same

API, Logs, UI, And Inference Findings

The same-model early-exit request produced 2 worker summaries, matching the
two-participant self-fill cap. The divergent request produced 4 summaries
because accounting includes both two draft calls and two refinement calls; it
does not imply four distinct nodes. Reduction ran once. Streaming deliberately
does not carry post-hoc x-moa-* headers, but emitted a valid OpenAI event
sequence.

The tool request selected one actor and returned structurally correct arguments.
Across the dedicated three-attempt reliability run, all 12 model=mesh phases
passed. Two model=auto tool-result phases omitted the fixture codeword; this is
consistent with weak-model output reliability and is not hidden by the aggregate.

No final log contained a structured error/fatal event or panic/fatal text.
Non-JSON lines were expected TUI/progress output. carrack used 3.8 GiB on its
RTX 5090 and 0.9 GiB on its RTX 3080 at the final snapshot. The Orin management
API reported roughly 59.3 GB usable VRAM per node.

Defects And Anomalies

Defect Severity Reproduction Expected / actual Release impact
Shipped quality path remains parity S1 See evals/moa-openrouter/RESULTS.md lines 427–492 Headline improvement / 9W-59T-12L, p=0.66 Blocks the claimed result; document itself calls it merge-blocking
Chat schema accepts absent/wrong messages S2 POST {"model":"mesh"} or string messages 400 / 200 fabricated unrelated prose Contract and safety defect
aarch64 release consistency failure S2 just check-release Pass / expected ...cuda.tar.gz, configured ...cuda-13.tar.gz Blocks clean release validation
Generic CUDA recipe stamps CUDA 12 by default S2 CUDA 13 host, just release-runtime-build cuda without override Detect installed toolkit / incompatible CUDA 12 manifest Fresh runtime rejected until explicit environment correction
model=auto tool-result omissions on 0.6B S3 Reliability harness, 3 attempts 12/12 / 10/12 Small-model route limitation; model=mesh was 12/12
Release build emits 28 unfulfilled lint-expectation warnings S3 just release-host-build on Orins Clean release output / warnings Build hygiene only; explicit Clippy gates pass

Risk And Follow-Up Register

Risk / gap Required action Status
Harness win does not survive shipped orchestration Diff exact prompt bytes and execution decisions, close parity gap, rerun the same study OPEN, blocking
Single-judge and absent raw trial-output risk Publish per-trial artifacts and rerun with a second judge OPEN, blocking for statistical claim
No live big-model quality comparison Run candidate through two verified 24–32B participants and a controlled baseline OPEN
Admission behavior only unit-tested here Add live two-big-plus-small topology assertion and record selected endpoint identities OPEN
OpenAI request schema too permissive Reject absent/non-array messages before MoA routing and add regression tests OPEN
Release metadata inconsistencies Fix CUDA asset parity and toolkit-major default/detection OPEN

Final Gate Assessment

Gate Result Evidence / rationale
All feature claims dispositioned PARTIAL Runtime behavior covered; statistical lift and live admission quality not proven
Canonical packages proven on real hosts PASS Three extracted CUDA 13 products run on two architectures
APIs, logs, and inference proven PARTIAL Core paths pass; request schema defect remains; no visual UI check
Candidate private mesh proven PASS Three nodes, same mesh ID, two peers each, failure/rejoin tested
Required mixed-version compatibility NOT_APPLICABLE Homogeneous PR request; no claimed protocol/ABI change
No open S0/S1 defect FAIL Headline shipped-path quality claim remains unsupported/parity
Required CI lanes terminal PASS Required GitHub checks passed; irrelevant matrix lanes skipped
Residual risk accepted and bounded FAIL Core improvement claim requires new evidence, not a waiver

Sign-Off

Decision: NOT_READY for merge/release on the stated quality-improvement
claim.
The live three-node MoA feature is functional and can remain running for
further investigation. Exact next actions: fix the shipped-path parity gap and
API schema defect, make the release consistency gate pass, publish/recompute
per-trial evidence with a second judge, then repeat a live capable-model mesh
comparison.

Self-fill could re-add the same physical node (or an endpoint already backing
the sole worker), turning ONE box into a fake 2-worker committee that hits it
twice for near-identical drafts. Catastrophic for mesh mode: a lone node must
degrade to single-model serving, never pretend to be a committee.

Rewrote self_fill_from_extra_instances to build the pool from DISTINCT physical
endpoints only: the local skippy port (if this node serves the model and
context fits) plus each distinct remote peer from hosts_for_model. If fewer
than two distinct endpoints serve the model, the pool stays the single worker
and MoA degrades to single-model. A single endpoint can no longer appear twice.

Also fixes the CodeRabbit self-fill bugs (re-adds same peer; skipped context
eligibility) — context fit is now checked on the local endpoint, and endpoints
are distinct by construction.

71 moa_gateway tests pass; clippy -D warnings and fmt clean.
A `model=mesh` request with no `messages`, a non-array `messages`, or an empty
array fell through to the workers and fabricated a 200 answer from nothing
(homelab validation API-1). try_handle_moa now requires a present, non-empty
`messages` array and returns 400 otherwise, before any model call.

Verified live: {"model":"mesh"}, string messages, and [] all now 400; a valid
request still returns 200. 71 moa_gateway tests pass; clippy -D warnings + fmt
clean.
@michaelneale

Copy link
Copy Markdown
Collaborator Author

@ndizazzo the cuda thing seems unrelated, and can you try that again with non trivial model as i can't see that model yielding much at all, so probably results are too noisy.

@michaelneale

michaelneale commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@ndizazzo yeah the claim is only for larger models combining (20B ish) unfortunately, so need to test with that (still probing myself). Also want to make sure this improves over the main branch.

but this reminds me to try it with larger combos... so standby.. (in the mean time feel free to try it with larger - and some subsequent fixes)

Width sprint (evals/moa-openrouter, aggregator qwen3-8b, 8B peers, shipped
committee path) shows the fan-out cap was throttling the pools that benefit
most:

  6× diverse 8B vs best member: 12W/65T/2L, p=0.013  (wins)
  4× diverse 8B:                 5W/73T/0L, p=0.06   (marginal)
  2× diverse 8B:                 2W/77T/1L, p=1.0    (null)

Small, weak drafts need WIDTH — more independent proposals — before
aggregation clears the best member. A verified big model, by contrast, wins at
2 and gains nothing past ~4.

- committee_cap is now tier-aware: COMMITTEE_CAP_SMALL=6 for all-small pools,
  COMMITTEE_CAP_BIG=4 when a verified big model is present (replaces the flat
  MAX_COMMITTEE_WORKERS=4).
- eval harness: the three per-trial judge comparisons now run concurrently
  (tokio::join!) instead of three serial awaits — ~3x faster judging phase, no
  behaviour change.

Also measured (recorded in RESULTS.md): the refine round never beats single
aggregation at 8B (refine-vs-single null in every cell) — Hermes' cheaper
single-synth cadence matches Together's layered shape here; and at 8B diversity
matters (6 diverse >> 6 same), unlike mid-scale.

72 moa_gateway tests (new: committee_cap_is_wide_for_small_pools_tight_for_big);
clippy -D warnings and fmt clean.
@michaelneale

michaelneale commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Update: width sprint + review fixes (since ed90d75e)

Addressed the homelab report and pushed several fixes. Key finding first.

Small-model meshes DO improve — with width, not depth

The homelab run used Qwen3-0.6B, an order of magnitude below the smallest model in our data (8B). Our committed table shows an 8B floor (2–3× 8B is null; 4× wins), so a 0.6B null is the expected result, not a design signal.

A fresh sprint (aggregator qwen3-8b, 8B-class peers, shipped committee path, position-swapped judge, length-noted) found the real lever is committee width:

pool single-agg vs best member refine vs single-agg
2× 8B diverse 2W/77T/1L, p=1.0 null
4× 8B diverse 5W/73T/0L, p=0.06 null
6× 8B diverse 12W/65T/2L, p=0.013 null
6× 8B same 4W/75T/1L, p=0.38 null

Three results:

  1. Six diverse 8B models beat the best single member (p=0.013). The old flat MAX_COMMITTEE_WORKERS=4 throttled exactly the small pools that need width. Cap is now tier-aware: 6 for all-small, 4 when a verified big model is present (a 24–32B pair already wins at 2).
  2. The refine round never earns its serial costrefine vs single-agg is null in every cell. Hermes' single-aggregation cadence matches Together's layered shape here at half the latency. (Noted for follow-up; not changed in this PR.)
  3. At 8B, diversity matters (6 diverse 12W/2L ≫ 6 same 4W/1L) — unlike mid-scale where Self ≈ Mixed.

Review fixes pushed

  • i386 P1 ×2: tiering now uses verified GGUF tensor-sum size gossiped via parameter_count_b (not name parsing); cap_committee ranks by size, not tool_use. Name fallback removed per follow-up — no verified size ⇒ ranked weakest. Validated: cached 3B→3.21, 4B→4.21; gemma-4-E4B name says 4B but sums to 7.5B (proves the name heuristic was unsafe).
  • Iron law: self-fill rebuilt to use distinct physical endpoints only — a single node can never fake a 2-worker committee; it degrades to single-model. (Also closes the two CodeRabbit self-fill bugs.)
  • API-1: model=mesh with missing/non-array/empty messages now returns 400, not a fabricated 200. Verified live.
  • CodeRabbit: pool.rs extracted; eval judges parallelised.

Still open (agreed)

  • Capable-model (24–32B) shipped-path run for the headline quality claim — length-controlled single judge (AlpacaEval-style), needs paid runs, separate effort.
  • aarch64 check-release parity + CUDA-toolkit-default: pre-existing, not touched by this branch (check-release passes on this HEAD; branch changes no release/packaging code).

Full data + method in evals/moa-openrouter/RESULTS.md.

michaelneale and others added 2 commits August 5, 2026 20:23
…is dead cost

The width sprint measured refine-vs-single-aggregation as null in every 8B cell
(2/4/6 workers, diverse and same). Small pools win by WIDTH under a single
aggregation, not by the extra serial refine pass. Previously RefinementPolicy::
Auto ran the round for ALL-small pools — paying two synthesis passes for no
measured gain.

Auto now refines only for a homogeneous pool that is NOT all-small (i.e.
same-model at real scale, where correlated drafts + the round measured 48/2 vs
35/10). All-small and diverse pools skip it — matching Hermes' cheaper
single-synth cadence, where refine buys nothing.

Effect: an all-small mesh turn drops from 2 serial synthesis passes to 1,
roughly halving added latency, with no measured quality loss.

Tests: auto_skips_an_all_small_pool (was auto_refines_...); the 5 all-small
mechanics sim tests (straggler/grace/degradation) pinned with Always so they
still exercise the round; big-pool gate tests keep Auto. 178 + sim tests pass;
clippy -D warnings and fmt clean.
@michaelneale

michaelneale commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@ndizazzo — thanks for the thorough homelab validation. Re-review requested; here's what changed since ed90d75e, mapped to your report:

API-1 (S2, schema too permissive) — fixed. model=mesh with missing / non-array / empty messages now returns 400 before any model call, not a fabricated 200. Verified live.

MOA-6 (admission only unit-tested) — the tiering it depends on is now hardened: verified GGUF tensor-sum size gossiped via parameter_count_b drives admission/cap (not name parsing), Unknown/unsized ranks weakest, and self-fill uses distinct physical endpoints only (a single node can never fake a committee — degrades to single-model).

MOA-8 (S1, shipped quality parity) — the stale RESULTS.md parity block predated later fixes; still, I agree the shipped-path quality win at capable scale isn't independently proven here and remains open (needs a paid 24–32B shipped-path run, length-controlled single judge). Not claiming it's closed.

On the 0.6B topology: that model is an order of magnitude below the smallest in our data (8B), and our committed table shows an 8B floor (2–3× is null, 4× marginal, 6× wins). So the null you observed at 0.6B is the expected result, not a design signal — a fresh width sprint (aggregator qwen3-8b, 8B peers, shipped committee path) shows 6× diverse 8B beats the best member, 12W/2L p=0.013; the fan-out cap is now tier-aware (6 small / 4 big) so small meshes aren't throttled, and the refine round is skipped where it measured null.

aarch64 check-release + CUDA-12-default — confirmed pre-existing and unrelated: check-release passes on this HEAD and the branch touches no release/packaging code. Flagging for a separate fix, not this PR.

Branch is merged current with main and CI is green. Full data/method in evals/moa-openrouter/RESULTS.md. Would appreciate another pass when you have a moment.

michaelneale and others added 7 commits August 6, 2026 10:28
Three shipped-path bugs meant `handle_turn` never ran the committee it was
supposed to. Found by measuring the shipped entrypoint at capable scale.

1. Grace pre-empted synthesis. `first_answer_grace` armed on the FIRST answer
   and finalized the turn, so on fast backends it fired ~every turn and shipped
   one worker's text. Measured 80/80 EarlyExit at 32B scale. Now grace
   finalizes on TOOL turns only; on answer turns it is a collection deadline —
   stop waiting for the tail, then synthesize what arrived. Private-mesh window
   widened 3s -> 10s so a normal committee completes before it arms.

2. Role-tiered draft budgets (Fast 256 / Specialist 512) truncated the drafts
   that synthesis consumes. Those tiers existed only to make the grace
   fast-path cheap; grace no longer finalizes answer turns, so every answer-turn
   worker now gets the full budget.

3. Answer turns shipped one worker verbatim when drafts agreed. Agreeing drafts
   are the best input to synthesis, not a reason to skip it. Answer turns with
   >=2 workers always synthesize now. Also drops the worker preamble from the
   reducer prompt — the synthesizer is not one of the parallel answerers, and
   the contradictory framing cost a weak aggregator.

Measured through `moa::handle_turn` at production defaults, 40 prompts x 2
draws, out-of-family judge, length-controlled:

  32B/24B/35B-MoE/14B pool vs best member:  71W / 8T / 1L   p<0.0001
    (was 26W/14T/40L before these fixes; wins 28/29 even when MoA is SHORTER,
     so it is not a length artifact)

Robustness contracts unchanged and still verified: slow worker cannot stall a
turn, lone survivor answers, hanging refiner cannot hold the turn, patience
expiry releases held consensus.
Measured through the shipped path (OpenRouter, 8B-class peers with an 8B
reducer, 40 preregistered prompts x 2 draws, out-of-family position-swapped
judge, vs the pool's best member alone):

  2x 8B:  0W/43T/37L  p<0.0001
  6x 8B:  5W/52T/23L  p=0.0009

The committee never won and lost about a third of decided trials, with
consistently shorter answers (3236-3372 chars vs ~4070 solo). A capable pool is
the opposite (71W/8T/1L, p<0.0001), so this is a statement about *this*
configuration -- a weak reducer synthesizing weak drafts -- not about
small-model MoA in general. The untested cell is small peers with a strong
reducer; if a mesh gains a big-tier model the pool is no longer all-small and
MoA engages again.

Rather than ship a measured regression, an all-small pool now collapses to its
single strongest member, so the caller degrades to serving that model directly.

Also corrects the private-mesh grace assertion to the widened 10s default.

The earlier "6x 8B beats its best member (12W/2L, p=0.013)" harness result did
not replicate: the same rig on the same 40 tasks now gives 3W/76T/1L (p=0.63),
with 11 of 40 tasks flipping verdict. It rested on ~14 decided trials out of 80
(the rest ties) and was a single unreplicated run. Withdrawn.
…ed results

The width-sprint headline (12W/2L, p=0.013) did not replicate: same rig, same
tasks, same pool now gives 3W/76T/1L (p=0.63), 11 of 40 tasks flipping. It rested
on ~14 decided trials from a single unreplicated run.

Records the shipped-path all-small measurements that motivated the best-member
gate (2x: 0W/37L, 4x: 5W/12L, 6x: 5W/23L), scopes the claim to weak-reducer
configurations, and documents known methodology limitations (tie bucket conflates
failures, per-draw vs per-prompt significance, unverified best member, single judge).
…imeout

Public-mesh comparison against released v0.74.0 (same 3B model, same prompts,
`model=mesh`) exposed a liveness regression I introduced earlier today:

  released v0.74.0:  4.0s / 3.2s / 11.3s   (reducer never ran)
  branch (N-1 gate): 61.0s / 6.7s / 61.7s  (reducer ran, SHORTER answers)

`min_grace_answers` gates whether grace can ARM at all, so any value above 1 is
a liveness hazard. With N-1 on a 6-worker public-mesh pool where two peers never
returned, only 4 answers ever arrived, grace could never arm, and the turn rode
`worker_timeout` (60s) instead. The two 61s turns had 6 workers/4 ok and 6/3; the
fast 6.7s turn had 4 workers/3 ok, where N-1=3 was satisfiable. 3/3 consistent.

Width comes from the grace WINDOW (10s), not a count gate: healthy peers land
inside it and are all synthesized, while a dead peer costs 10s rather than 60s.
This also restores the exact configuration the capable-pool win was measured
under (71W/8T/1L, p<0.0001) — that run predates the N-1 gate — and the gate
bought nothing on small pools either (8W/17L with it, 9W/17L without).
…claim

MOA_GATEWAY.md described behaviour that no longer holds:

- "requires >=2 distinct models, returns 503 if fewer" -> a single model (or an
  all-small pool collapsed to its best member) now degrades: the virtual `mesh`
  name is rewritten to a real served model and routed normally. Only a node
  serving nothing returns 503.
- name-derived tiering -> tiering now comes from verified GGUF tensor sums
  gossiped as `parameter_count_b`, split at 10B, with unverified sizes ranking
  small so an unparseable alias cannot pose as big-tier. Name parsing mis-tiered
  real models (gemma-4-E4B stores 7.5B, not 4B).
- documents pool shaping in order (admission, all-small best-member fallback,
  committee cap) with the measured numbers behind each.
- records that answer turns pack every worker at the full budget.
- stale peer-timeout row (15s) -> 60s worker timeout with the 10s grace window.

RESULTS.md: the width-sprint section now carries an explicit WITHDRAWN banner
pointing at the replication failure, so the 6x8B number cannot be cited from the
middle of the file.
@michaelneale michaelneale changed the title MoA: ensembling that improves as mesh nodes join MoA: mesh answers improve as capable nodes join Aug 6, 2026
The tool path is deliberately asymmetric — route to the single best tool-caller
rather than fan out and vote, because voting on tool calls measured
null-to-harmful. That is only safe if structured `tool_calls` still come back
intact when several models are on the wire, so this asserts the contract through
the shipped `handle_turn` with a real pool: a tool prompt yields exactly one
well-formed call whose arguments parse as JSON and whose name is an offered
tool, and a no-tool prompt does not invent one.

Measured live (OpenRouter, both cells pass):

  4x 8B pool  -> dispatched=4 ok=3 reducer=true
    list_dir({"path":"src"})
    search({"path":".","pattern":"MeshError::Timeout\\(.*?\\)"})
    run_command({"cmd":"pytest --verbose"})
    no_tool_concept: no call invented
  4x 24-35B pool -> dispatched=1 ok=1 reducer=true
    same shapes, single actor

The dispatch difference is `ReferencePolicy::Auto` working as measured: a
small-tier actor gets tool-free advisors (+0.017 uplift), a big-tier actor acts
alone (advisors measured -0.037 there). The 4-worker cell also survived a worker
flaking (ok=3 of 4) and still emitted a valid call, which is the ensemble-active
case that was previously only covered by replay fixtures.

Asserts the contract, not a quality delta — the tool chosen may legitimately
differ from a single model's first move, so only a non-offered tool or malformed
arguments fail.
@michaelneale
michaelneale merged commit 96ef622 into main Aug 6, 2026
45 checks passed
@michaelneale
michaelneale deleted the feat/moa-synthesis branch August 6, 2026 08:41
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.

3 participants