fix(mesh): skip filtered peers in gossip dial loop to unwedge --auto - #602
Merged
Conversation
PR #576 added two gossip-ingest filters (version floor + idle-transitive client). They correctly reject ghost peers at ingest, but their reject path `state.peers.remove(&id) + return` interacts badly with the dial loop later in the same gossip exchange: ```text attempt_run_auto_join → join_with_retry → connect_to_peer(invite_peer) → initiate_gossip → gossip_round_trip (30s timeout wraps the WHOLE continuation including the dial loop below) → apply_gossip_announcements → apply_announced_peers (PR #576 filters remove ghosts here) → for each ann in their_announcements: maybe_connect_discovered_peer(addr) → connect_to_peer(addr) → if state.peers.contains_key(&peer_id) { return Ok(()) } → else: 30s connect timeout per unreachable host ``` `connect_to_peer`'s fast-path keys on `state.peers.contains_key`. PR #576's filter actively removes filtered peers from `state.peers` before the dial loop, so the fast-path no longer fires — and each unreachable ghost address gets a real 30s connect timeout, sequentially. With ~445 v0.57.x ghosts in a payload, that's hours of being stuck inside `initiate_gossip`, which means `attempt_run_auto_join` never returns and `run_auto` never reaches `LaunchPlan` / model load. ## Fix Apply the same filter inside `maybe_connect_discovered_peer`, before the dial. If a peer announcement would be rejected by the version-floor or idle-transitive-client filter at ingest, do not dial it from the discovery loop either. This is a single 18-line skip at the call site — no change to the ingest filters, no shared state with ingest beyond the predicates that are already public to the module. ## Validation Local 4-run reliability test on this branch (`serve --auto --model Qwen/Qwen2.5-3B-Instruct-GGUF:qwen2.5-3b-instruct-q4_k_m`): | Run | loaded | ready | peers | inference | |----:|--------|-------|------:|------------------| | 1 | ✅ | ✅ | 37 | "Surething" | | 2 | ✅ | ✅ | 43 | "Sure thing." | | 3 | ✅ | ✅ | 44 | "Sure thing." | | 4 | ✅ | ✅ | 43 | "Surething." | Peer table stays clean (~40 peers, all v0.60+, no v0.57.x ghosts) — the PR #576 ingest filters do their job. Last CKPT reached: `CKPT 9: after setup_run_auto_console_state` (full startup sequence completes). `cargo test -p mesh-llm-host-runtime --lib` — 1424/1424 pass, including PR #576's existing filter tests plus a new regression test that exercises the skip path: * `maybe_connect_discovered_peer_skips_filtered_announcements` — calls the dial entry with a below-floor announcement and an idle-transitive announcement, asserts both return well under the 30 s connect timeout and neither produces an entry in `state.connections` or `state.peers`. ## Supersedes This is the proper fix for the wedge described in PR #601, which disabled the ingest filters entirely as a short-term workaround. With this PR the filters stay on, the local peer table stays clean, and `--auto` is reliable. PR #601 can be closed once this lands.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes an intermittent mesh-llm serve --auto startup wedge on the public mesh by preventing the gossip discovery dial loop from attempting to connect to peers that are intentionally filtered out of the local peer table.
Changes:
- Add a pre-dial filter in
maybe_connect_discovered_peerto skip peers below the version floor and idle transitive clients (avoids sequential 30s dial timeouts on “ghost” peers). - Add a regression test ensuring filtered announcements are skipped quickly and don’t create
connections/peersentries.
…mplexity The previous commit pushed `maybe_connect_discovered_peer` to a cognitive complexity of 27 (limit 20). Extract the filter check into a small associated helper `discovered_peer_is_filtered` so the call site stays compact and clippy is happy with `-D warnings`. No behavior change. `cargo test -p mesh-llm-host-runtime --lib gossip::` \u2014 24/24 pass. `cargo clippy -p mesh-llm-host-runtime --all-targets -- -D warnings` clean.
ndizazzo
approved these changes
May 20, 2026
ndizazzo
left a comment
Collaborator
There was a problem hiding this comment.
Interesting - so whoever/whatever is doing this likely recognized this aspect of mesh and added the flood of nodes knowing it would potentially starve starting up
Collaborator
Author
|
@ndizazzo maybe, but I don't think so - it would always have, just was luck it sometimes worked I think, I don't think anything really changed. |
michaelneale
added a commit
that referenced
this pull request
May 20, 2026
* origin/main: fix(mesh): skip filtered peers in gossip dial loop to unwedge `--auto` (#602) docs(agents): clarify just build vs release-build for serious testing (#599) build: ozempic — slim binary -42 MB / -47 MB (#592) fix(runtime): proxy through mesh during serve --auto startup (#591) chore(code-quality): Set max 200 line limit for long methods (#595) fly size bump (#590) agents is now up to date (#588) fix(ci): fix CI PR cleanup job to delete in batches (#587)
michaelneale
added a commit
that referenced
this pull request
May 21, 2026
* main: docs(AGENTS): add confidence-testing recipe for routing/MoA/gossip changes (#613) ci(sdk-smoke): install lld in macOS swift smoke job (#610) MoA: mesh mode and many inference critical fixes, and quic keep alive (#566) fix(ci): small update for lint rule (#608) mockup: Reserves high-fidelity UI mockup (#560) Add advisory capacity evaluation for model targets (#579) Harden Skippy layer package materialization cache (#583) fix(release): pin Windows CUDA to sccache-compatible version (#606) fix(build-windows): tolerate dead sccache server in CUDA retry path (#604) chore(version): synchronize version bump everywhere (#562) fix(mesh): skip filtered peers in gossip dial loop to unwedge `--auto` (#602) docs(agents): clarify just build vs release-build for serious testing (#599) build: ozempic — slim binary -42 MB / -47 MB (#592)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
mesh-llm serve --automodel load wedged intermittently on the live public mesh: process up, mesh chatter visible, but noLaunchPlanand no model load. After this PR,--autois reliable AND we keep PR #576's anti-spam (ghost peers stay out of the local table, routing, UI).This supersedes PR #601 (which worked around the wedge by disabling PR #576's filters entirely — that left ~640 ghost peers in the table).
Why
--autowedgesconnect_to_peer's fast-path keys onstate.peers.contains_key. PR #576's filter removes filtered peers fromstate.peersbefore the dial loop, so the fast-path no longer fires — and each unreachable ghost gets a real 30s connect timeout, sequentially. With ~445 v0.57.x ghosts in a payload,initiate_gossipis pinned for hours,attempt_run_auto_joinnever returns,run_autonever reachesLaunchPlan.Fix
Apply the same filter predicates inside
maybe_connect_discovered_peerbefore the dial. 18-line skip, single call site, no change to the ingest filters.Validation
Local 4-run reliability test (
serve --auto --model Qwen/Qwen2.5-3B-Instruct-GGUF:qwen2.5-3b-instruct-q4_k_m):SurethingSure thing.Sure thing.Surething.Peer table stays clean (~40 peers, all v0.60+, no v0.57.x ghosts). Full startup sequence completes (
CKPT 9: after setup_run_auto_console_statereached in every run).cargo test -p mesh-llm-host-runtime --lib— 1424/1424 pass, including:maybe_connect_discovered_peer_skips_filtered_announcementsexercises the skip path — a below-floor announcement and an idle-transitive announcement both return well under the 30s connect timeout, with no entries created instate.connectionsorstate.peers.Architecture
The bug was a hidden coupling between two independent gossip code paths:
update_transitive_peer(ingest) — knew about the filter and usedstate.peers.removeto enforce it.apply_gossip_announcements(dial loop) — keyed its short-circuit onstate.peers.contains_key, which the ingest had just invalidated for filtered peers.The fix decouples them: the dial loop now applies the filter independently of
state.peers. Both paths use the same predicates (version_allowed_for_rebroadcast,peer_is_idle_transitive_client), so adding a new filter dimension in the future is a single-place change.No protocol change. No mesh-compatibility change. Behaviour against older peers is unchanged — they were filtered before, they are still filtered, we just don't burn 30s timeouts dialing them now.
Supersedes
PR #601 (
micn/disable-gossip-ingest-filters).