Skip to content

Tighten small-mesh node layout to prevent overlap - #11

Closed
i386 wants to merge 1 commit into
Mesh-LLM:mainfrom
i386:codex/fix-overlapping-visualization-boxes
Closed

Tighten small-mesh node layout to prevent overlap#11
i386 wants to merge 1 commit into
Mesh-LLM:mainfrom
i386:codex/fix-overlapping-visualization-boxes

Conversation

@i386

@i386 i386 commented Mar 23, 2026

Copy link
Copy Markdown
Collaborator

Before:
image

After:
image

@michaelneale

Copy link
Copy Markdown
Collaborator

Merged to main via cherry-pick in 8d5ab1b

michaelneale added a commit that referenced this pull request May 21, 2026
…tion, dedup race, dead code) (#612)

* docs(README): mark `model: \"mesh\"` MoA as experimental

The MoA gateway is new and still being tuned (routing heuristics,
error shapes, tuning knobs). Flag it explicitly in the README so
readers do not assume the surface is stable.

* fix(moa): close panic surface in worker output normalization

PR #566 review (Copilot): the MoA crate had several latent panics
around untrusted parsed responses. This commit closes them in one
pass and adds regression tests for each.

Changes
-------

normalize.rs
* Single sanitize pass in `normalize_worker_output` runs on the result
  of *every* parse strategy (JSON, KV, heuristic), not just the
  heuristic path. The previous shape returned early on JSON/KV success
  and let non-finite confidences leak into the arbiter where
  `partial_cmp/total_cmp` could panic on `.unwrap()`. The new helper
  `sanitize_worker_output` clamps NaN/Inf confidence to 0.5 and
  collapses non-object `tool_arguments` (Null, primitives, arrays)
  to `Some({})`.
* New `extract_tool_arguments` helper replaces two dead
  `obj.get("arguments").cloned().or_else(\u2026)` chains in
  `try_json_parse`. The `or_else` branch was unreachable because
  `.cloned()` on `Some(Value::String(\u2026))` is already `Some`, so
  string-encoded JSON arguments leaked through unparsed.
  `extract_tool_arguments` now explicitly branches on String vs
  Object vs Null and parses the inner JSON when needed.

backend.rs
* `parse_retry_after` switched from `to_lowercase()` (Unicode-aware,
  can change UTF-8 byte length) to `to_ascii_lowercase()` (1:1 byte
  mapping). The earlier shape sliced the original string using the
  offset from the lowercased one, which could land mid-codepoint and
  panic for non-ASCII inputs before the marker.
* `extract_text_from_response` switched from direct-indexing
  `resp["choices"][0]["message"]` to `.pointer()` chaining,
  returning a structured `Err("malformed response: \u2026")` on missing
  fields instead of silently producing empty content or panicking
  downstream.

lib.rs
* `best_answer` now uses `total_cmp` instead of
  `partial_cmp(\u2026).unwrap()`. `total_cmp` is total over all f32 (NaN/
  Inf included), so even if a future caller bypasses
  `normalize_worker_output`, this site is panic-free.
* `tool_call_response` now explicitly handles every input shape
  callers can construct: object \u2192 serialize, Null \u2192 `"{}"`,
  primitive / array \u2192 `"{}"`, validated JSON string \u2192 pass through,
  invalid string \u2192 `"{}"`. Previously `Value::Null` serialized to
  the literal four-char string "null", which downstream OpenAI
  tool-call consumers reject.

Tests
-----

13 new tests (now 83/83 pass for the crate):
* normalize.rs: kv_path_clamps_nan_confidence, kv_path_clamps_inf_confidence,
  json_string_encoded_arguments_are_parsed_to_object,
  null_tool_arguments_become_none,
  primitive_tool_arguments_collapse_to_empty_object.
* backend.rs: parse_retry_after_handles_non_ascii_prefix_without_panic,
  extract_text_returns_err_on_missing_choices,
  extract_text_returns_err_on_empty_choices.
* lib.rs (new `response_builder_tests` mod):
  best_answer_does_not_panic_on_nan_confidence,
  tool_call_response_emits_object_args_for_null,
  tool_call_response_emits_object_args_for_primitive,
  tool_call_response_passes_through_string_form_when_valid,
  tool_call_response_rejects_invalid_string_form.

Validation
----------
* cargo test -p mesh-mixture-of-agents --lib: 83/83 pass
* cargo check -p mesh-llm: clean
* cargo clippy -p mesh-mixture-of-agents --all-targets -- -D warnings: clean
* cargo fmt --all -- --check: clean

* fix(moa): propagate failure signals to HTTP status, SSE, and tool-result reducer

PR #566 review (Copilot) \u2014 three related fixes that all touch how
MoA failures reach the caller.

1. HTTP status follows the body's failure signal, not TurnKind
-------------------------------------------------------------
`write_moa_response` previously only used HTTP 502 when
`TurnKind == Failed`. The tool-result reducer path
(`handle_tool_result`) can return `error_response(\u2026)` with
`TurnKind::ToolResult` when every reducer candidate fails \u2014 that
returned HTTP 200 with an in-band error body, so dumb clients that
only check the status code saw a "success".

New helper `is_moa_failure_body(body)` recognises the two canonical
failure signals MoA emits: a top-level `error` field, and / or
`choices[0].finish_reason == "error"`. Status decision now uses
this helper, so *every* error-shaped MoA response surfaces as 502
regardless of which sub-flow produced it.

2. SSE adapter propagates the original finish_reason
----------------------------------------------------
`send_moa_as_sse` used to hard-code `finish_reason: "stop"` (or
`"tool_calls"` if any tool_calls were present). SSE clients keyed
on `finish_reason` (Goose, OpenAI SDKs) therefore saw MoA failures
as successful completions.

Now the adapter reads `choices[0].finish_reason` from the response
body and propagates it ("error" wins over the tool_calls heuristic),
and when `is_moa_failure_body` is true it emits an explicit error
chunk (`{ object: chat.completion.chunk, choices: [], error: \u2026 }`)
before the final finish_reason chunk so SSE clients that scan deltas
for an `error` field see the failure too.

3. Tool-result reducer emits tool_calls whenever tool_name is set
-----------------------------------------------------------------
Both the tool-result path (`handle_tool_result`) and the
fanout/arbiter path (`resolve_decision`) had the same bug: a
`ToolProposal` from the reducer only became a real `tool_calls`
reply when *both* `tool_name` AND `tool_arguments` were present.
With `tool_arguments` missing, both paths silently fell back to a
`chat_response` carrying the reducer's prose \u2014 which agent harnesses
(Goose, OpenCode) ignore because they only act on `tool_calls`.

Now both paths emit `tool_calls` whenever `tool_name` is set, and
`tool_call_response` already collapses missing / non-object
arguments to `"{}"`. Behaviour is consistent across paths and agent
harnesses no longer lose tool calls to reducer prose.

Tests
-----
Four new `is_moa_failure_body` unit tests in moa_gateway.rs:
top-level error, finish_reason=error, success body, tool_calls body.

Validation
----------
* cargo test -p mesh-mixture-of-agents --lib: 83/83 pass
* cargo test -p mesh-llm-host-runtime --lib: 1446/1446 pass
* cargo clippy -p mesh-mixture-of-agents -p mesh-llm-host-runtime --all-targets -- -D warnings: clean
* cargo fmt --all -- --check: clean

* fix(moa): group aliases by canonical base before resolving backend

PR #566 review (Copilot, item #10): the worker pool builder committed
to a single alias per canonical model *before* trying to resolve a
backend. Two real failure modes:

1. Stale-peer drop. The shortest alias is advertised only by a peer
   that drops between gossip refresh and orchestration. The peer is
   gone, `hosts_for_model(alias)` returns empty, the model is
   silently removed from the worker pool, and longer-form aliases
   for the same canonical model from still-reachable peers are
   rejected as duplicates.

2. Forced QUIC hop. The local node advertises a longer convention
   (e.g. `unsloth/Qwen3-8B-GGUF:Q4_K_M`) while a peer advertises a
   shorter variant (e.g. `Qwen3-8B-Q4_K_M`). The shortest-name rule
   picks the peer alias, `add_worker_backend` looks for a local port
   under that specific string in `targets`, finds nothing, and
   forces a QUIC tunnel even though the model is locally served.

Fix: group all advertised aliases by canonical base first, then
within each group sort so the most likely optimization wins first
try (locally-served alias before remote, then shortest first as a
tiebreaker). The resolver walks the group in order and takes the
first alias that produces a backend, so an unreachable preferred
alias falls back to a reachable longer one instead of dropping the
model.

Refactor extracts a small `resolve_one_worker_from_aliases` helper
to keep `build_moa_config` under the cognitive-complexity limit.

Tests (4 new) in moa_gateway.rs:
* group_aliases_keeps_all_aliases_per_canonical_base
* group_aliases_prefers_locally_served_alias_even_when_longer
* group_aliases_falls_back_to_shortest_when_no_local
* group_aliases_distinct_models_stay_in_separate_groups

Validation
----------
* cargo test -p mesh-llm-host-runtime --lib moa_gateway: 13/13 pass
* cargo test -p mesh-llm-host-runtime --lib: 1450/1450 pass
* cargo clippy -p mesh-llm-host-runtime --all-targets -- -D warnings: clean
* cargo fmt --all -- --check: clean

* fix(moa): strip dead Session continuation/running-summary machinery

PR #566 review (Copilot, item #11): `Session::classify_turn` used a
`self.turns` counter to decide Fresh vs Continuation, but the
gateway constructs a fresh `Session` per inbound request and never
invokes `record_assistant_response` / `record_turn_outcome` in
production. As a result `turns` was always 1, `Continuation` never
fired, and the entire deterministic running-summary feature
(`accepted_facts`, `AcceptedFact`, `record_turn_outcome`,
`rebuild_summary`, `running_summary` accessor) silently never
executed. `pack_fast` had a `turn_count() > 1` branch that
appended the summary to the worker's system prompt; that branch was
dead too.

This commit reflects the gateway's actual design: MoA is
request-scoped, and the caller (Goose, OpenCode, an SDK) owns the
multi-turn loop and sends the full history each request. Continuation
context comes from `session.messages()`, not from a gateway-owned
summary.

Changes
-------
session.rs
* Drop `TurnType::Continuation` (only `Fresh` and `ToolResult`
  remain). The header comment documents the request-scoped lifetime.
* Drop fields: `turns`, `last_was_tool_call`, `accepted_facts`,
  `running_summary`.
* Drop methods: `record_assistant_response`,
  `record_turn_outcome`, `rebuild_summary`, `running_summary`,
  `accepted_facts`, `turn_count`,
  `has_unprocessed_tool_results`.
* Drop struct: `AcceptedFact`.
* Rewrite `ingest` to rebuild `pending_tools` from the
  caller-provided history each call (delta tracking only worked when
  Sessions were persisted across requests). Both assistant-emitted
  tool_calls and tool-result messages are picked up; agent harnesses
  unchanged.
* Simplify `classify_turn`: walk history backwards, return
  `ToolResult` if a `role: "tool"` message appears before any
  `role: "assistant"`, else `Fresh`.

lib.rs
* Drop the `Continuation` match arm.

context.rs
* Drop the `turn_count() > 1` running-summary injection from
  `pack_fast`. Context comes from `session.messages()`.

Tests
-----
* tool_result_turn test rewritten to not rely on the removed
  `record_assistant_response`; verifies the same behaviour with
  caller-provided history.
* No other test changes needed.

Net: 132 LoC of dead machinery removed.

Validation
----------
* cargo test -p mesh-mixture-of-agents --lib: 83/83 pass
* cargo test -p mesh-mixture-of-agents (incl. integration): all pass
* cargo test -p mesh-llm-host-runtime --lib: 1450/1450 pass
* cargo check -p mesh-llm: clean
* cargo clippy -p mesh-mixture-of-agents --all-targets -- -D warnings: clean
* cargo fmt --all -- --check: clean

* fix(moa): tighten error code, linearize strip_thinking, sanitize header names, drop dead branch

PR #566 review (Copilot) \u2014 batch of MEDIUM cleanups.

mesh-mixture-of-agents
----------------------
* `error_response` now takes a `code: &str` parameter and the two call
  sites pass distinct constants:
  - `MOA_ERR_ALL_WORKERS_FAILED` for the fanout/arbiter failure path.
  - `MOA_ERR_ALL_REDUCERS_FAILED` for the tool-result reducer path.
  Previously both paths emitted `code = "all_workers_failed"`, which
  was misleading for clients branching on `error.code`.

* `strip_thinking` rewritten as a single linear pass over the input.
  The previous shape rebuilt the entire string on every think block
  (`format!` + `replace` in a loop), which is O(n*k) for long worker
  outputs with many `<think>` blocks. Three new tests cover the new
  shape: orphan close-tag handling, fifty-block linear behaviour, and
  UTF-8 preservation through multibyte content.

mesh-llm-host-runtime
---------------------
* `network::openai::transport`: header NAMES are now validated against
  the RFC 7230 tchar grammar via a new `is_valid_header_name` helper.
  Names that fail the grammar are dropped with a tracing warning rather
  than written verbatim. CR/LF in header values is still stripped.\n  Both `send_json_ok_with_headers` and\n  `send_json_with_status_and_headers` route through a shared\n  `append_safe_header(\u2026)` so the validation can't be bypassed by a\n  future caller. Four new tests cover the validator and the safe\n  header writer.\n\n* `network::openai::moa_gateway::send_moa_as_sse` now reuses\n  `append_safe_header` so the SSE adapter gets the same name validation\n  as the JSON writers.\n\n* `network::openai::ingress::try_intercept_moa`: dropped the\n  unreachable `if let Some(_unused_stream) = \u2026 { tracing::error!(\u2026) }`\n  branch. `try_handle_moa` self-gates on the model name and the outer\n  gate guarantees it matches, so the inner call always returns\n  `None` here. Replaced with `let _ = \u2026.await;` and a comment\n  explaining the invariant.\n\nValidation\n----------\n* cargo test -p mesh-mixture-of-agents --lib: 86/86 pass (3 new)\n* cargo test -p mesh-llm-host-runtime --lib: 1454/1454 pass (4 new)\n* cargo clippy -p mesh-mixture-of-agents -p mesh-llm-host-runtime\n  --all-targets -- -D warnings: clean\n* cargo fmt --all -- --check: clean

* docs(moa): align stale comments and doc claims with current code

PR #566 review (Copilot) \u2014 batch of LOW comment/doc corrections so
future readers don't trust outdated narrative.

* mesh-llm-host-runtime/src/network/openai/moa_gateway.rs:
  `try_handle_moa` doc said "returns `true` if handled" but the
  signature is `Option<TcpStream>`. Rewrote the doc to describe the
  actual contract: `Some(stream)` means "not MoA, fall through",
  `None` means "MoA consumed the stream and responded; do not respond
  again".

* model-ref/src/lib.rs: comment claimed the function "emits a
  lowercase selector", but the implementation slices from the\n  original (case-preserving) stem and lowercasing is only used for\n  marker-position lookup. Reworded so the comment matches the code.\n\n* skippy-cache/src/resident/prefix.rs: regression-test comment\n  referenced a "4*min_tokens floor" \u2014 that derivation was replaced by\n  a hard `MIN_CTX_FOR_CELL_CAP = 8192` threshold in a follow-up\n  commit. Updated to describe the floor that actually ships.\n\n* mesh-llm-host-runtime/src/inference/skippy/family_policy.rs:\n  inline comment said the 16-entry cap "does not fully eliminate"\n  unified-KV starvation and that the "proper fix is out of scope".\n  The proper fix (token-based budget via `max_resident_tokens` in\n  `ResidentCacheConfig::from_stage`) shipped in PR #566. Updated to\n  describe entry-count as the coarse lever and `max_resident_tokens`\n  as the complementary fine-grained budget.\n\n* docs/design/MOA_GATEWAY.md:\n  - Role assignment section claimed "the largest of the big tier gets\n    Strong". The impl only partitions small vs big, no\n    within-tier sort by parameter count. Reworded to describe the\n    actual heuristic and call out the future option.\n  - Test plan said "29 unit tests" with a stale per-area breakdown.\n    Updated to 86 with current coverage areas, and added a note to\n    bump the number when adding tests.\n\nValidation\n----------\n* cargo test -p mesh-mixture-of-agents --lib: 86/86 pass\n* cargo test -p mesh-llm-host-runtime --lib: 1454/1454 pass\n* cargo test -p skippy-cache --lib: 13/13 pass\n* cargo test -p model-ref --lib: 10/10 pass\n* cargo clippy -p mesh-mixture-of-agents -p mesh-llm-host-runtime\n  -p skippy-cache -p model-ref --all-targets -- -D warnings: clean\n* cargo fmt --all -- --check: clean

* fix(moa): drop malformed tool_calls instead of inserting empty-id placeholders

PR #612 review (Copilot) \u2014 two follow-ups.

1. session.rs: malformed tool_calls in caller history
-----------------------------------------------------
The earlier shape defaulted missing/empty `id` and `function.name` to
`""` and still pushed a `PendingToolCall`. With adversarial or buggy
caller history this had two real consequences:

* Two malformed calls share `call_id == ""` and become
  indistinguishable, so any later `role: "tool"` whose
  `tool_call_id` is also missing matches the first one rather than
  the intended call.
* The `tool_call_response` wire-shape invariant (non-empty function
  name) is violated downstream.

Now both `assistant`-side ingestion and `tool`-side matching skip
entries with missing or empty `id`/`tool_call_id` (and `assistant`
ingestion also skips entries with empty `function.name`), emitting a
`tracing::warn!` so the issue is visible in logs. Well-formed history
is unaffected.

2. normalize.rs: doc/code mismatch on tool_arguments
----------------------------------------------------
The `extract_tool_arguments` doc claimed missing/null `arguments`
collapses to `Some({})`. The code returns `None` and relies on the
downstream `tool_call_response` to substitute `"{}"` when
serializing the wire shape. The wire output is correct either way,
but the doc misled future maintainers about the invariant. Rewrote
the comment to describe the actual invariant ("`None` or an object;
`None` means emit `{}` at wire time") so a future refactor doesn't
reintroduce the literal-"null" bug.

Regression test
---------------
`malformed_tool_calls_are_dropped_not_collapsed_to_empty_id` covers
four malformed shapes in one history (missing id, missing
function.name, empty id, plus an orphaned tool result) and asserts
only the one well-formed call survives and the orphaned result
doesn't attach.

Validation
----------
* cargo test -p mesh-mixture-of-agents --lib: 87/87 pass (+1 new)
* cargo test -p mesh-llm-host-runtime --lib: 1454/1454 pass
* cargo clippy -p mesh-mixture-of-agents -p mesh-llm-host-runtime
  --all-targets -- -D warnings: clean
* cargo fmt --all -- --check: clean

Live end-to-end validation
--------------------------
Built locally and deployed to the 2-node private mesh (M4 Qwen3-8B +
Studio MiniMax). All four agent harness shapes from PR #566/#612
exercised the changed code paths:

* mini-agent.py model=auto \u2014 2 turns, correct.
* mini-agent.py model=mesh \u2014 2 turns, correct.
* mini-agent2.py model=mesh \u2014 4 turns multi-file, correct.
* Goose model=mesh \u2014 1 tool call, correct.

No regressions in MoA fan-out / tool-result reducer routing.

* fix(moa): route streaming failures to HTTP 502 (drop in-band SSE error)

PR #612 review feedback (Nick) \u2014 two related findings on the streaming
failure path.

1. Streaming MoA failures returned HTTP 200
-------------------------------------------
`write_moa_response` previously routed all streaming responses through
`send_moa_as_sse`, which always emits `HTTP/1.1 200 OK` because
SSE clients expect 200 before they start parsing the event stream.
That meant `stream: true` MoA failures (`all_workers_failed`,
`all_reducers_failed`, reducer hedge exhaustion) arrived as a 200
SSE that *happened* to carry `finish_reason: "error"` \u2014 dumb HTTP
clients saw a "successful" stream and didn't realise inference had
failed.

The body is fully available before we decide how to write it, so we
can collapse failure-shaped streaming responses to a non-streaming
HTTP 502 JSON response with the structured error body. This matches
the OpenAI API shape (failures on streaming endpoints come back as a
single non-streaming JSON error response with the right HTTP status)
and means streaming and non-streaming MoA failures are now consistent
at the HTTP layer.

2. Error-only SSE chunk could break OpenAI-shape clients
--------------------------------------------------------
The previous error chunk emitted on failure was
`{ object: chat.completion.chunk, choices: [], error: \u2026 }`.
Many OpenAI client SDKs assume every `chat.completion.chunk` has
`choices[0]` and index blindly into it, so an empty `choices: []`
array crashes those clients with an index error.

With finding #1's routing change, failure-shaped bodies never reach
`send_moa_as_sse` anymore, so the error-chunk emission is dead code.
Drop it. `send_moa_as_sse` now does one clear thing: emit a single
delta chunk + a `finish_reason: "stop" | "tool_calls"` stop chunk,
both with a real `choices[0]`. A `debug_assert!` pins the invariant
in tests.

Tests
-----
Four new unit tests covering the four corners of the routing
decision:

* `streaming_success_routes_to_sse`
* `streaming_failure_routes_to_json_502_not_sse`
* `non_streaming_success_routes_to_json_200`
* `non_streaming_failure_routes_to_json_502`

Validation
----------
* cargo test -p mesh-llm-host-runtime --lib: 1458/1458 pass (4 new)
* cargo test -p mesh-mixture-of-agents --lib: 87/87 pass
* cargo clippy ... --all-targets -- -D warnings: clean
* cargo fmt --all -- --check: clean

Live end-to-end validation on 2-node mesh (M4 + Studio MiniMax):
* Non-streaming MoA happy path: HTTP 200 + structured response.
* Streaming MoA happy path: HTTP 200 + `text/event-stream` + `[DONE]`.
* mini-agent.py model=mesh: 2 turns, correct.
* mini-agent2.py model=mesh: 4 turns multi-file, correct.
* Goose model=mesh: 1 tool call, correct.

The failure path is exercised by unit tests; production-triggering
streaming failures would require fault injection beyond what the
harness covers, but the routing logic and the SSE invariant are
locked down at the code level.
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.

2 participants