Skip to content

fix(mesh): skip filtered peers in gossip dial loop to unwedge --auto - #602

Merged
michaelneale merged 2 commits into
mainfrom
micn/skip-filtered-peers-in-dial-loop
May 20, 2026
Merged

fix(mesh): skip filtered peers in gossip dial loop to unwedge --auto#602
michaelneale merged 2 commits into
mainfrom
micn/skip-filtered-peers-in-dial-loop

Conversation

@michaelneale

Copy link
Copy Markdown
Collaborator

What

mesh-llm serve --auto model load wedged intermittently on the live public mesh: process up, mesh chatter visible, but no LaunchPlan and no model load. After this PR, --auto is 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 --auto wedges

attempt_run_auto_join → join_with_retry → connect_to_peer(invite_peer)
  → initiate_gossip (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 removes filtered peers from state.peers before 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_gossip is pinned for hours, attempt_run_auto_join never returns, run_auto never reaches LaunchPlan.

Fix

Apply the same filter predicates inside maybe_connect_discovered_peer before 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):

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). Full startup sequence completes (CKPT 9: after setup_run_auto_console_state reached in every run).

cargo test -p mesh-llm-host-runtime --lib — 1424/1424 pass, including:

  • PR mesh: drop peers below v0.60.0 from gossip ingest and re-broadcast #576's existing 6 filter tests still green.
  • New regression: maybe_connect_discovered_peer_skips_filtered_announcements exercises the skip path — a below-floor announcement and an idle-transitive announcement both return well under the 30s connect timeout, with no entries created in state.connections or state.peers.

Architecture

The bug was a hidden coupling between two independent gossip code paths:

  1. update_transitive_peer (ingest) — knew about the filter and used state.peers.remove to enforce it.
  2. apply_gossip_announcements (dial loop) — keyed its short-circuit on state.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).

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.
Copilot AI review requested due to automatic review settings May 20, 2026 05:52

Copilot AI 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.

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_peer to 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/peers entries.

Comment thread crates/mesh-llm-host-runtime/src/mesh/gossip.rs Outdated
…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.
@michaelneale michaelneale added the blocker blocking other PRs label May 20, 2026

@ndizazzo ndizazzo 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.

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

@michaelneale

Copy link
Copy Markdown
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
michaelneale merged commit c60ae2f into main May 20, 2026
18 checks passed
@michaelneale
michaelneale deleted the micn/skip-filtered-peers-in-dial-loop branch May 20, 2026 06:20
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 20, 2026
* 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)
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocker blocking other PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants