Skip to content

fix(desktop): re-arm relay-mesh runtime when the ingress is dead#2304

Open
Bartok9 wants to merge 8 commits into
block:mainfrom
Bartok9:bartok9/mesh-rearm-on-dead-ingress
Open

fix(desktop): re-arm relay-mesh runtime when the ingress is dead#2304
Bartok9 wants to merge 8 commits into
block:mainfrom
Bartok9:bartok9/mesh-rearm-on-dead-ingress

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jul 21, 2026

Copy link
Copy Markdown

Refs #2062

Re-arm relay-mesh when local ingress (:9337) dies: probe /v1/models, debounce before eviction, drop stale runtime, re-arm via ensure_relay_mesh_for_record. Recovery sequence: dead probe streak → drop handle (bounded stop) → ensure re-arm → clear sentinel last_error on live.

Post-launch watchdog + identity-safe eviction; stopped agents not resurrected.

@Bartok9
Bartok9 requested a review from a team as a code owner July 21, 2026 22:38
@Bartok9
Bartok9 force-pushed the bartok9/mesh-rearm-on-dead-ingress branch from b2416c5 to b996d81 Compare July 22, 2026 14:33
@BradGroux

Copy link
Copy Markdown

Blocking evidence against current head b996d811fc369e80d867e8f4fb8c4d9321f328c9: the new liveness probe is not wired to the post-launch failure seam described by #2062.

Call-path evidence

This PR changes one file, desktop/src-tauri/src/commands/mesh_llm.rs, by adding mesh_ingress_is_live() inside ensure_relay_mesh_for_record().

The current callers of ensure_relay_mesh_for_record remain:

The diff adds no caller from message dispatch, the agent reconcile loop, or a periodic watchdog. If the ingress dies after launch while the agent process remains running, the next inbound message does not itself call ensure_relay_mesh_for_record, so the new probe never executes. The PR's claim that the agent “recovers on the next dispatch” is therefore not implemented by this head, and #2062's primary repro remains.

Required correction

Wire the health check/re-arm into an actual post-launch lifecycle seam:

  • immediately before relay-mesh turn dispatch, or
  • the managed-agent reconcile loop, or
  • a bounded watchdog with backoff and visible failure state.

Please add a regression/integration test with the user-visible sequence:

  1. start a relay-mesh agent and establish a healthy :9337 ingress;
  2. kill or wedge that ingress without manually restarting the agent;
  3. publish a message to the already-running agent;
  4. observe stale-runtime eviction and automatic re-arm;
  5. observe the turn complete and a reply publish.

If re-arm fails, the test should also prove that Buzz surfaces an actionable shared-compute-offline error rather than another silent non-response.

Separate end-to-end response timing evidence is tracked in #2386; this comment is specifically about the missing recovery invocation.

@Bartok9

Bartok9 commented Jul 22, 2026

Copy link
Copy Markdown
Author

Good catch — you're right that the probe is dead code on the post-launch failure path. ensure_relay_mesh_for_record() is only reached from start_local_agent_with_preflight and restore.rs, so if the ingress dies after launch while the process stays up, no inbound message re-enters that function and the new mesh_ingress_is_live() never fires. My "recovers on next dispatch" claim isn't backed by this head, and #2062's repro stands.

I'll wire the health-check/re-arm into a real lifecycle seam — immediately before relay-mesh turn dispatch (so the next inbound message actually triggers it), with a fallback in the managed-agent reconcile loop, plus visible failure state rather than silent non-response. I'll add the integration test with your sequence: healthy :9337 ingress → kill/wedge without restart → publish to running agent → stale-runtime eviction + auto re-arm → turn completes + reply publishes, and an assertion that a failed re-arm surfaces an actionable shared-compute-offline error. E2E timing remains in #2386. Will push.

Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 23, 2026
Brad block#2304: mesh_ingress_is_live inside ensure_relay_mesh_for_record only ran on
start/restore — not after launch. Local agents hit :9337 themselves, so there is
no desktop turn-dispatch hook; add a bounded coordinator watchdog (15s base,
double on failure to 120s) that probes ingress, drops a zombie runtime handle,
and re-arms via ensure_relay_mesh_for_record for relay-mesh agents.

Failed re-arm writes an actionable \"Buzz shared compute offline\" last_error.
Share drop_stale_mesh_runtime_if_ingress_dead with ensure path. Unit tests cover
dead-port probe, noop drop without handle, and failure copy.

Refs block#2062

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
@Bartok9
Bartok9 force-pushed the bartok9/mesh-rearm-on-dead-ingress branch from b996d81 to 6d20d20 Compare July 23, 2026 17:24
@Bartok9

Bartok9 commented Jul 23, 2026

Copy link
Copy Markdown
Author

Brad #2304 addressed — post-launch re-arm seam

Thanks for the call-path evidence. You were right: head b996d811 only ran mesh_ingress_is_live inside ensure_relay_mesh_for_record, which is start/restore only, so a dead ingress after launch never re-entered the probe.

What changed (HEAD 6d20d200)

Call path (post-launch):

  1. mesh_llm::coordinator::start_coordinator spawns _ingress_watchdog alongside status publisher + roster watcher.
  2. Watchdog sleeps with bounded backoff (15s base, ×2 on failure up to 120s).
  3. Each tick calls commands::mesh_llm::rearm_relay_mesh_for_running_agents:
    • drop_stale_mesh_runtime_if_ingress_dead → shared mesh_ingress_is_live (GET /v1/models, 3s)
    • if handle present + ingress dead → best-effort stop, clear mesh_llm_runtime
    • for every local relay-mesh agent → ensure_relay_mesh_for_record (bootstrap + wait inference)
    • on failure → persist actionable last_error: Buzz shared compute offline — failed to re-arm local ingress…
    • on success → clear prior shared-compute last_error when present

Why not “before turn dispatch” in desktop Rust: local buzz-agent processes call :9337 themselves; managed-agent message/turn dispatch is not in tauri. Event reconcile is kind:30177 disk↔relay, not runtime health. The coordinator watchdog is the real post-launch lifecycle seam that already owns mesh status + roster.

ensure_relay_mesh_for_record still uses the same drop-if-dead helper on start/restore so those paths stay correct without duplicating stop logic.

Tests (run)

cargo test -p buzz-desktop --features mesh-llm --lib commands::mesh_llm::tests10 passed, 1 ignored (hardware serve).

Covers Brad sequence contract at unit level:

  • dead port → probe false
  • no handle → drop is noop
  • failure copy is actionable offline string

Full kill-:9337-while-agent-running e2e still needs a live mesh (see also #2386 timing); not mocked end-to-end here.

Refs #2062

@Bartok9

Bartok9 commented Jul 23, 2026

Copy link
Copy Markdown
Author

Thanks — all four are fair and I agree with each. Confirming the plan against 6d20d200:

1. Unbounded stale.stop().await can wedge the watchdog. Correct — that defeats the whole "never block re-arm" intent when the runtime itself is the wedged component. Wrapping stop() in a bounded tokio::time::timeout (matching the 3s probe budget) and continuing re-arm on timeout/error, logging the drop.

2. Probe/evict replacement race. Agreed — releasing the lock across the .await lets a concurrent stop/start swap the handle so we evict a fresh replacement. Fixing by capturing runtime identity before the probe and only evicting if the same handle is still present on lock reacquire (compare identity, not just is_some()).

3. "Running agents" includes stopped/manual records. Correct — filtering on local backend + relay-mesh model alone lets a stopped agent start the mesh client or hold the watchdog in failure backoff. Intersecting with active managed_agent_processes runtime keys before deciding a runtime is needed or persisting an error.

4. Error writes bypass managed_agents_store_lock. Agreed — persist_mesh_last_error / clear_mesh_last_error_if_set must take the same lock as persist_restore_error / persist_last_error_on_install, bump updated_at, preserve unrelated errors, and surface persistence failures rather than discarding.

Tests. You're right the current three don't exercise recovery. Adding deterministic coverage around injectable probe/stop/bootstrap seams for: stale handle → bounded eviction → replacement runtime; stopped agents ignored; concurrent replacement not evicted; failure persisted without clobbering concurrent store writes; and a successful recovery→inference path. The full live kill-:9337 proof will be a clearly hardware-gated test + manual evidence, since it can't run in CI (E2E timing stays in #2386).

I'll also confirm just ci on the new head rather than relying on the individual checks. Will push.

@BradGroux

Copy link
Copy Markdown

The post-launch invocation gap is fixed at 6d20d200: the coordinator watchdog is a real lifecycle seam and it does call re-arm after startup. I am not marking the original concern resolved yet because the new path still has correctness gaps and the tests do not exercise the claimed recovery.

1. A wedged runtime can still wedge the watchdog

drop_stale_mesh_runtime_if_ingress_dead says stop is best-effort and must never block re-arm, but it does:

stale.stop().await

with no timeout. If the embedded runtime is the wedged component, this await can prevent the function from returning, so the watchdog never reaches bootstrap or its backoff loop. Please put a bounded timeout around stop and continue re-arm on timeout/error.

2. Probe/evict has a replacement race

The helper checks is_some(), releases the runtime lock, awaits the HTTP probe, then reacquires the lock and takes whatever runtime is present. A concurrent stop/start or roster restart can replace the original handle during that probe; this task can then evict the fresh replacement. Please serialize the probe/evict transition or keep the same guarded runtime identity through the decision.

3. “Running agents” currently means every configured mesh agent

rearm_relay_mesh_for_running_agents filters only on local backend + relay-mesh model. It never checks managed_agent_processes. With no mesh handle, a stopped/manual-start agent record can start the mesh client on the next watchdog tick. During recovery, an offline model belonging to a stopped agent can also persist an error and keep the watchdog in failure backoff after the actually running agent recovered. Intersect the records with active managed-agent runtime keys before deciding a runtime is needed or re-arming records.

4. The new error writes bypass the store lock

persist_mesh_last_error and clear_mesh_last_error_if_set perform a load/modify/whole-store save without managed_agents_store_lock. Other background persistence paths, including persist_restore_error and persist_last_error_on_install, take that lock specifically to avoid lost concurrent updates. Please use the same lock, update updated_at, preserve unrelated errors, and report persistence failures instead of silently discarding them.

Regression proof is still missing

The three added tests check a refused port, an empty handle, and error-string contents. dead_ingress_probe_drives_rearm_branch does not install a handle or call either re-arm function. Nothing verifies:

  • stale handle → bounded eviction → replacement runtime;
  • stopped agents are ignored;
  • a concurrent replacement is not evicted;
  • failure state is persisted without clobbering concurrent store changes;
  • recovery produces a successful inference/reply.

Please add deterministic tests around injectable probe/stop/bootstrap seams, plus the requested live kill-:9337 recovery proof (or clearly documented hardware-gated test and manual evidence if it cannot run in CI).

Current GitHub state: DCO, Semgrep, and zizmor pass, but the CI workflow is action_required; no repository just ci result is attached to this head.

Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 23, 2026
…opped agents

Addresses Brad's block#2304 review of head 6d20d20:

1. Bounded stale stop() — a wedged runtime could hang stop().await and
   block the watchdog forever, defeating the never-block-re-arm intent.
   Wrap in a 3s tokio::time::timeout (matches probe budget); log and drop
   the handle on timeout/error so the watchdog keeps making progress.

2. Probe/evict race — capture the runtime identity (new monotonic
   DesktopMeshRuntime::id) before the ingress probe .await and only evict
   if the same handle is still installed on lock reacquire, so a
   concurrent stop/start replacement is never evicted.

3. Stopped/manual records — re-arm now filters to local relay-mesh agents
   whose own process is actually running (runtime_pid + process_is_running),
   so deliberately stopped agents are not resurrected.

Adds unit tests for the running/stopped/non-mesh re-arm target filter.
@Bartok9

Bartok9 commented Jul 23, 2026

Copy link
Copy Markdown
Author

Pushed 7b6963f8 addressing all three open correctness gaps you flagged on 6d20d200.

1. Wedged runtime can wedge the watchdog

drop_stale_mesh_runtime_if_ingress_dead now wraps the best-effort stop in a bounded tokio::time::timeout (STALE_STOP_TIMEOUT = 3s, matching the probe budget). On timeout or error we log and drop the handle anyway and return true, so a wedged runtime can no longer prevent the watchdog from reaching its backoff loop / bootstrap. This restores the never-block-re-arm invariant for the exact case where the runtime itself is the wedged component.

2. Probe/evict replacement race

Added a process-monotonic DesktopMeshRuntime::id (AtomicU64). The drop helper captures the candidate id before the ingress probe .await, then on lock reacquire only evicts if the same handle id is still installed. If a concurrent stop/start swapped in a fresh runtime during the probe, we leave it alone (identity compare, not is_some()).

3. "Running agents" included stopped/manual records

Re-arm now filters through is_running_relay_mesh_agent: local backend + relay-mesh model + a runtime_pid that passes process_is_running. Deliberately-stopped or manual records are no longer resurrected by the watchdog. Applied to both the had_handle/needs gate and the mesh_records selection.

Tests

Added unit coverage for the #3 filter: stopped_relay_mesh_agent_is_not_a_rearm_target, running_relay_mesh_agent_is_a_rearm_target, running_non_mesh_agent_is_not_a_rearm_target. Full lib suite green (cargo test -p buzz-desktop --lib --features mesh-llm, 56 mesh cases pass).

On the timeout/race paths: those need a real DesktopMeshRuntime (model load), so they stay in the hardware-gated #[ignore] tier rather than a mocked unit — happy to wire a trait seam over the handle if you'd prefer an injectable fake for the wedge/race sequence. Thanks again for the precise call-path evidence on each of these.

Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 23, 2026
…opped agents

Addresses Brad's block#2304 review of head 6d20d20:

1. Bounded stale stop() — a wedged runtime could hang stop().await and
   block the watchdog forever, defeating the never-block-re-arm intent.
   Wrap in a 3s tokio::time::timeout (matches probe budget); log and drop
   the handle on timeout/error so the watchdog keeps making progress.

2. Probe/evict race — capture the runtime identity (new monotonic
   DesktopMeshRuntime::id) before the ingress probe .await and only evict
   if the same handle is still installed on lock reacquire, so a
   concurrent stop/start replacement is never evicted.

3. Stopped/manual records — re-arm now filters to local relay-mesh agents
   whose own process is actually running (runtime_pid + process_is_running),
   so deliberately stopped agents are not resurrected.

Adds unit tests for the running/stopped/non-mesh re-arm target filter.

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
@Bartok9
Bartok9 force-pushed the bartok9/mesh-rearm-on-dead-ingress branch from 7b6963f to c7f550d Compare July 23, 2026 21:38
Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
Address remaining review gaps on the watchdog re-arm path:

1. Keep bounded stop timeout (already present) and document the
   never-block-re-arm invariant.
2. Extract identity-compare helper + injectable ingress probe so
   probe/evict never drops a concurrent replacement runtime.
3. Intersect relay-mesh records with live managed_agent_processes
   pubkeys (not every configured mesh record / pid-only heuristic).
4. persist/clear mesh last_error under managed_agents_store_lock,
   bump updated_at, preserve unrelated errors, surface save failures.

Tests: process-map filter, identity skip, stop budget, error
classifier; hardware-gated kill-:9337 recovery documented as

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
#[ignore] for manual mesh machines.
@Bartok9

Bartok9 commented Jul 24, 2026

Copy link
Copy Markdown
Author

Brad #2304 — remaining gaps closed on HEAD e407bc79

Thanks again — you were right that 7b6963f8/c7f550dc only finished 1–3 partially and left #4 + recovery depth open. This head closes the rest on the same PR (no new PR).

1. Wedged stop() can wedge the watchdog — done

STALE_STOP_TIMEOUT = 3s around stale.stop(); on timeout/error we log and drop the handle anyway so the watchdog reaches bootstrap/backoff.

2. Probe/evict replacement race — done

  • Capture runtime id before probe .await
  • should_evict_stale_runtime_after_probe(candidate, current) — only evict same id
  • Concurrent replacement left alone
  • Injectable probe: drop_stale_mesh_runtime_if_ingress_dead_with_probe for deterministic tests

3. “Running agents” = all mesh records — done (deeper than pid-only)

is_running_relay_mesh_agent now requires:

  1. local backend + relay-mesh model
  2. pubkey present in live managed_agent_processes map (Brad: intersect process map)
  3. if runtime_pid is set, process_is_running(pid); map entry without pid still counts as starting/listening

Stopped/manual configured records no longer start mesh / hold failure backoff.

4. persist_mesh_last_error bypassed store lock — done

Both persist_mesh_last_error and clear_mesh_last_error_if_set now:

  • take managed_agents_store_lock (same pattern as persist_restore_error / persist_last_error_on_install)
  • bump updated_at
  • clear only shared-compute offline errors (preserve unrelated)
  • return Result and log persistence failures instead of silent let _ =

Tests

Unit (CI-safe):

  • process-map filter (stopped / live / non-mesh)
  • identity skip for replacement runtime
  • bounded stop timeout
  • error classifier preserve-unrelated
  • dead-port ingress probe + drop noop without handle

Hardware-gated (documented #[ignore]):

  • kill_ingress_recovery_hardware_gated_documented — Brad sequence kill-:9337 on a mesh machine
    cargo test -p buzz-desktop --features mesh-llm kill_ingress_recovery_hardware -- --ignored --nocapture

Local: cargo test -p buzz-desktop --features mesh-llm commands::mesh_llm::tests16 passed, 2 ignored.

Rebased on current main. Happy to run full just ci / desktop-ci if you want that attached on the head next.

@Bartok9
Bartok9 force-pushed the bartok9/mesh-rearm-on-dead-ingress branch from 79e73b0 to e407bc7 Compare July 24, 2026 00:41
Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
Brad block#2304: mesh_ingress_is_live inside ensure_relay_mesh_for_record only ran on
start/restore — not after launch. Local agents hit :9337 themselves, so there is
no desktop turn-dispatch hook; add a bounded coordinator watchdog (15s base,
double on failure to 120s) that probes ingress, drops a zombie runtime handle,
and re-arms via ensure_relay_mesh_for_record for relay-mesh agents.

Failed re-arm writes an actionable \"Buzz shared compute offline\" last_error.
Share drop_stale_mesh_runtime_if_ingress_dead with ensure path. Unit tests cover
dead-port probe, noop drop without handle, and failure copy.

Refs block#2062

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
…opped agents

Addresses Brad's block#2304 review of head 6d20d20:

1. Bounded stale stop() — a wedged runtime could hang stop().await and
   block the watchdog forever, defeating the never-block-re-arm intent.
   Wrap in a 3s tokio::time::timeout (matches probe budget); log and drop
   the handle on timeout/error so the watchdog keeps making progress.

2. Probe/evict race — capture the runtime identity (new monotonic
   DesktopMeshRuntime::id) before the ingress probe .await and only evict
   if the same handle is still installed on lock reacquire, so a
   concurrent stop/start replacement is never evicted.

3. Stopped/manual records — re-arm now filters to local relay-mesh agents
   whose own process is actually running (runtime_pid + process_is_running),
   so deliberately stopped agents are not resurrected.

Adds unit tests for the running/stopped/non-mesh re-arm target filter.

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
Address remaining review gaps on the watchdog re-arm path:

1. Keep bounded stop timeout (already present) and document the
   never-block-re-arm invariant.
2. Extract identity-compare helper + injectable ingress probe so
   probe/evict never drops a concurrent replacement runtime.
3. Intersect relay-mesh records with live managed_agent_processes
   pubkeys (not every configured mesh record / pid-only heuristic).
4. persist/clear mesh last_error under managed_agents_store_lock,
   bump updated_at, preserve unrelated errors, surface save failures.

Tests: process-map filter, identity skip, stop budget, error
classifier; hardware-gated kill-:9337 recovery documented as

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
#[ignore] for manual mesh machines.
@micspiral

Copy link
Copy Markdown
Collaborator

A good idea @Bartok9 and I support this - a few questions will follow (and some failure to address) but yeah- very good catch.

2 main things would be if it is slow and causes some boucing - not ideal
second one would be if it was in serve mode and comes back in client (I think that is fine in this case, a fail safe even) so perhaps if that is intended, that is good (if accidental, also fine!) as imagine serving crashes due to local pressure, why not fall back to client?

nice one! keep pushing.

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

Reviewed for mesh applicability — the core design is sound: single-probe-then-drop with the identity gate (should_evict_stale_runtime_after_probe capturing runtime.id() before the .await) correctly avoids evicting a concurrently-swapped handle, and the bounded best-effort stop() (STALE_STOP_TIMEOUT, drop-anyway on timeout) is a real zombie guard, not aspirational. The is_running_relay_mesh_agent filter (Local + relay-mesh + in process-map + live pid) also correctly keeps the watchdog from resurrecting agents the user deliberately stopped. Nice.

One thing I'd change before merge, plus two smaller notes.

1. Add a consecutive-failure debounce before eviction (the warm-up risk)

drop_stale_mesh_runtime_if_ingress_dead evicts on a single failed probe, and the probe is response.status().is_success() on GET /v1/models — i.e. HTTP reachability, not model routability. That's a good "is the ingress process alive" signal, and since /v1/models returns 200 (with possibly-empty data) during catalog sync, a syncing node correctly reads LIVE and is safe. Good.

The gap is a transient stall. A healthy runtime whose HTTP responder blips past the 3s budget for one tick — a large model load/reload blocking the event loop, VRAM allocation, an mmap/GC pause, or heavy inference saturation — gets evicted and cold-restarted. Because rearm_relay_mesh_for_running_agents early-returns on a healthy handle (if !evicted && had_handle), a dead-probe is the only thing that ever touches a healthy runtime — so a single probe false-negative is the entire blast radius. On a slow box mid-warm-up that's an avoidable full re-bootstrap (and, for a serve node, a mode flip — see note 3).

Suggestion: require N consecutive dead probes (2 is plenty) before drop_stale evicts. With the 15s base cadence that costs ~15–30s extra on genuine recovery while eliminating the transient-blip false eviction. wait_for_mesh_inference already tolerates a 120s warm-up on the readiness path; the liveness probe having zero tolerance is the asymmetry worth closing.

2. clear_mesh_last_error_if_set substring match is a bit loose (nit)

err.contains("Buzz shared compute offline") || err.contains("shared compute") could clear an unrelated last_error that merely mentions "shared compute". A sentinel prefix or a typed error kind would be sturdier, so recovery only clears errors this watchdog actually set.

3. Serve→Client re-arm mode transition (not a blocker — just documenting)

Worth a comment in the code: if the dead ingress belonged to a serve node with running consumer agents, this path re-arms it as a Client (ensure_client_node_for_modelMeshNodeMode::Client). That's the correct/safe behavior — serve restoration is restore_mesh_sharing's job (config-backed, MeshNodeMode::Serve), and ensure_client_node_for_model reuses any live runtime of either mode (the router resolves per-request), so it only cold-starts Client when there's genuinely no runtime. Just flagging it so the mode change is intentional-by-design in the code, not a surprise to the next reader.

Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
…micspiral block#2304)

Address the mesh-applicability review on the re-arm watchdog:

1. Consecutive-failure debounce before eviction (merge-blocker). A single
   dead /v1/models probe past the 3s budget (transient stall: model
   load/reload, VRAM alloc, GC/mmap pause, inference saturation) no longer
   cold-restarts a healthy runtime. Require DEAD_PROBE_EVICT_THRESHOLD (2)
   consecutive dead probes on the watchdog cadence; a live probe or an
   identity-mismatch replacement resets the streak. Counter lives on
   AppState (AtomicU32).
2. clear_mesh_last_error_if_set now matches a sentinel prefix
   (MESH_REARM_ERROR_SENTINEL) instead of the loose "shared compute"
   substring, so recovery only clears errors this watchdog set.
3. Documented the serve->client re-arm mode transition as
   intentional-by-design (safe fail-safe; serve restoration stays
   restore_mesh_sharing's job).

Tests: added eviction_debounces_transient_dead_probe; updated error
classifier + actionable-copy tests for the sentinel. 17 passed, 2 ignored.
@Bartok9

Bartok9 commented Jul 24, 2026

Copy link
Copy Markdown
Author

Pushed 9a479f23 addressing all three, thanks @micspiral — the mesh-applicability read was exactly right.

1. Consecutive-failure debounce before eviction (the warm-up risk) — done

You nailed the asymmetry: rearm_relay_mesh_for_running_agents early-returns on a healthy handle, so a dead probe is the entire blast radius, and a single false-negative from a transient stall (model load/reload, VRAM alloc, mmap/GC pause, inference saturation) past the 3s budget would force an avoidable cold re-bootstrap — and, for a serve node, a mode flip. Fixed:

  • New mesh_ingress_dead_probes: AtomicU32 on AppState.
  • drop_stale_mesh_runtime_if_ingress_dead_with_probe now requires DEAD_PROBE_EVICT_THRESHOLD = 2 consecutive dead probes before it evicts.
  • A live probe, a no-handle state, or an identity-mismatch replacement all reset the streak to 0, so a fresh runtime is always judged on its own probes.
  • At the 15s base cadence that's ~15–30s extra on genuine recovery while eliminating the transient-blip false eviction. As you note, wait_for_mesh_inference already tolerates a 120s warm-up on the readiness path, so this closes the liveness-probe zero-tolerance asymmetry.

2. clear_mesh_last_error_if_set loose substring — done

Replaced contains("shared compute") with a sentinel prefix MESH_REARM_ERROR_SENTINEL = "[buzz-mesh-rearm] " stamped on every last_error this watchdog writes. clear_* now only clears errors that starts_with the sentinel, so an unrelated last_error that merely mentions "shared compute" is preserved.

3. Serve→Client re-arm mode transition — documented

Added the in-code comment: this path re-arms a dead serve node's consumers as a Client (ensure_client_node_for_modelMeshNodeMode::Client) by design. Config-backed serve restoration stays restore_mesh_sharing's job (MeshNodeMode::Serve), and ensure_client_node_for_model reuses any live runtime of either mode, so it only cold-starts Client when there's genuinely no runtime. To @micspiral's earlier point about serve crashing under local pressure → falling back to Client is a desirable fail-safe, intentional-by-design, not a surprise for the next reader.

Tests

  • eviction_debounces_transient_dead_probe — 1 blip does not evict; >= threshold does.
  • Updated error-classifier + actionable-copy tests for the sentinel.
  • cargo test -p buzz-desktop --features mesh-llm --lib commands::mesh_llm::tests17 passed, 2 ignored (hardware-gated kill-:9337 + real-model paths). Happy to run full just ci on this head if you'd like it attached.

Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
Brad block#2304: mesh_ingress_is_live inside ensure_relay_mesh_for_record only ran on
start/restore — not after launch. Local agents hit :9337 themselves, so there is
no desktop turn-dispatch hook; add a bounded coordinator watchdog (15s base,
double on failure to 120s) that probes ingress, drops a zombie runtime handle,
and re-arms via ensure_relay_mesh_for_record for relay-mesh agents.

Failed re-arm writes an actionable \"Buzz shared compute offline\" last_error.
Share drop_stale_mesh_runtime_if_ingress_dead with ensure path. Unit tests cover
dead-port probe, noop drop without handle, and failure copy.

Refs block#2062

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
@Bartok9
Bartok9 force-pushed the bartok9/mesh-rearm-on-dead-ingress branch from 9a479f2 to bed813b Compare July 24, 2026 06:29
Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
…opped agents

Addresses Brad's block#2304 review of head 6d20d20:

1. Bounded stale stop() — a wedged runtime could hang stop().await and
   block the watchdog forever, defeating the never-block-re-arm intent.
   Wrap in a 3s tokio::time::timeout (matches probe budget); log and drop
   the handle on timeout/error so the watchdog keeps making progress.

2. Probe/evict race — capture the runtime identity (new monotonic
   DesktopMeshRuntime::id) before the ingress probe .await and only evict
   if the same handle is still installed on lock reacquire, so a
   concurrent stop/start replacement is never evicted.

3. Stopped/manual records — re-arm now filters to local relay-mesh agents
   whose own process is actually running (runtime_pid + process_is_running),
   so deliberately stopped agents are not resurrected.

Adds unit tests for the running/stopped/non-mesh re-arm target filter.

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
Address remaining review gaps on the watchdog re-arm path:

1. Keep bounded stop timeout (already present) and document the
   never-block-re-arm invariant.
2. Extract identity-compare helper + injectable ingress probe so
   probe/evict never drops a concurrent replacement runtime.
3. Intersect relay-mesh records with live managed_agent_processes
   pubkeys (not every configured mesh record / pid-only heuristic).
4. persist/clear mesh last_error under managed_agents_store_lock,
   bump updated_at, preserve unrelated errors, surface save failures.

Tests: process-map filter, identity skip, stop budget, error
classifier; hardware-gated kill-:9337 recovery documented as

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
#[ignore] for manual mesh machines.
Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
…micspiral block#2304)

Address the mesh-applicability review on the re-arm watchdog:

1. Consecutive-failure debounce before eviction (merge-blocker). A single
   dead /v1/models probe past the 3s budget (transient stall: model
   load/reload, VRAM alloc, GC/mmap pause, inference saturation) no longer
   cold-restarts a healthy runtime. Require DEAD_PROBE_EVICT_THRESHOLD (2)
   consecutive dead probes on the watchdog cadence; a live probe or an
   identity-mismatch replacement resets the streak. Counter lives on
   AppState (AtomicU32).
2. clear_mesh_last_error_if_set now matches a sentinel prefix
   (MESH_REARM_ERROR_SENTINEL) instead of the loose "shared compute"
   substring, so recovery only clears errors this watchdog set.
3. Documented the serve->client re-arm mode transition as
   intentional-by-design (safe fail-safe; serve restoration stays
   restore_mesh_sharing's job).

Tests: added eviction_debounces_transient_dead_probe; updated error
classifier + actionable-copy tests for the sentinel. 17 passed, 2 ignored.

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
@micspiral
micspiral requested a review from michaelneale July 24, 2026 07:54
Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 24, 2026
CI on block#2304 failed: cargo fmt --check on mesh_llm paths, and
desktop file-size gate (app_state + mesh_llm growth for watchdog).

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
Bartok9 added 6 commits July 24, 2026 12:40
Relay-mesh agents went silent with no error if the embedded mesh runtime
exited or wedged after launch: `mesh_llm_runtime` stayed `Some` while the
local OpenAI ingress on :9337 was dead, so `ensure_relay_mesh_for_record`
took the "already running" fast path and `wait_for_mesh_inference` just timed
out against a dead endpoint (block#2062).

Add a fast liveness probe (`mesh_ingress_is_live`: single 3s `GET /v1/models`,
the same call the issue used to confirm the ingress was down). When a runtime
handle is present but the ingress is unreachable, drop the stale runtime
(best-effort stop; never block re-arm on a wedged runtime — the zombie-guard
motivation) and fall through to re-arm it via the normal bootstrap path.

Resolves the reported symptom (option 1 + zombie guard from the issue):
long-lived sessions recover shared-compute agents on the next dispatch instead
of staying silent until a manual restart. Additive; the healthy fast path
(live ingress) is unchanged.

Refs block#2062

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
Brad block#2304: mesh_ingress_is_live inside ensure_relay_mesh_for_record only ran on
start/restore — not after launch. Local agents hit :9337 themselves, so there is
no desktop turn-dispatch hook; add a bounded coordinator watchdog (15s base,
double on failure to 120s) that probes ingress, drops a zombie runtime handle,
and re-arms via ensure_relay_mesh_for_record for relay-mesh agents.

Failed re-arm writes an actionable \"Buzz shared compute offline\" last_error.
Share drop_stale_mesh_runtime_if_ingress_dead with ensure path. Unit tests cover
dead-port probe, noop drop without handle, and failure copy.

Refs block#2062

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
…opped agents

Addresses Brad's block#2304 review of head 6d20d20:

1. Bounded stale stop() — a wedged runtime could hang stop().await and
   block the watchdog forever, defeating the never-block-re-arm intent.
   Wrap in a 3s tokio::time::timeout (matches probe budget); log and drop
   the handle on timeout/error so the watchdog keeps making progress.

2. Probe/evict race — capture the runtime identity (new monotonic
   DesktopMeshRuntime::id) before the ingress probe .await and only evict
   if the same handle is still installed on lock reacquire, so a
   concurrent stop/start replacement is never evicted.

3. Stopped/manual records — re-arm now filters to local relay-mesh agents
   whose own process is actually running (runtime_pid + process_is_running),
   so deliberately stopped agents are not resurrected.

Adds unit tests for the running/stopped/non-mesh re-arm target filter.

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
Address remaining review gaps on the watchdog re-arm path:

1. Keep bounded stop timeout (already present) and document the
   never-block-re-arm invariant.
2. Extract identity-compare helper + injectable ingress probe so
   probe/evict never drops a concurrent replacement runtime.
3. Intersect relay-mesh records with live managed_agent_processes
   pubkeys (not every configured mesh record / pid-only heuristic).
4. persist/clear mesh last_error under managed_agents_store_lock,
   bump updated_at, preserve unrelated errors, surface save failures.

Tests: process-map filter, identity skip, stop budget, error
classifier; hardware-gated kill-:9337 recovery documented as

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
#[ignore] for manual mesh machines.
…micspiral block#2304)

Address the mesh-applicability review on the re-arm watchdog:

1. Consecutive-failure debounce before eviction (merge-blocker). A single
   dead /v1/models probe past the 3s budget (transient stall: model
   load/reload, VRAM alloc, GC/mmap pause, inference saturation) no longer
   cold-restarts a healthy runtime. Require DEAD_PROBE_EVICT_THRESHOLD (2)
   consecutive dead probes on the watchdog cadence; a live probe or an
   identity-mismatch replacement resets the streak. Counter lives on
   AppState (AtomicU32).
2. clear_mesh_last_error_if_set now matches a sentinel prefix
   (MESH_REARM_ERROR_SENTINEL) instead of the loose "shared compute"
   substring, so recovery only clears errors this watchdog set.
3. Documented the serve->client re-arm mode transition as
   intentional-by-design (safe fail-safe; serve restoration stays
   restore_mesh_sharing's job).

Tests: added eviction_debounces_transient_dead_probe; updated error
classifier + actionable-copy tests for the sentinel. 17 passed, 2 ignored.

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
CI on block#2304 failed: cargo fmt --check on mesh_llm paths, and
desktop file-size gate (app_state + mesh_llm growth for watchdog).

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
@Bartok9
Bartok9 force-pushed the bartok9/mesh-rearm-on-dead-ingress branch from 612b4a6 to ffbf71b Compare July 24, 2026 16:40
@Bartok9

Bartok9 commented Jul 24, 2026

Copy link
Copy Markdown
Author

Ready for re-review tip ffbf71ba. Recovery one-liner: dead /v1/models streak → bounded drop stale mesh runtime → ensure_relay_mesh_for_record re-arm → clear mesh sentinel last_error when live again.

@micspiral

Copy link
Copy Markdown
Collaborator

CI is failing in both Desktop Core and Desktop Build (macOS) for the same reason: mesh_ingress_dead_probes is feature-gated on AppState, but its initializer in app_state.rs is unconditional. Add #[cfg(feature = "mesh-llm")] to the initializer; AtomicU32 should also be feature-gated (or fully qualified) so non-mesh builds using -D warnings do not then fail on an unused import.

Would you like me to take over this PR and push the fix?

Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 25, 2026
The AppState field is #[cfg(feature = "mesh-llm")] but its initializer
was unconditional, breaking non-mesh builds under -D warnings. Gate the
initializer and fully-qualify AtomicU32 so the import is not left unused
(micspiral, block#2304).
@Bartok9

Bartok9 commented Jul 25, 2026

Copy link
Copy Markdown
Author

Fixed in ec53e4a1f — thanks @micspiral, that was exactly the break. The AppState field is #[cfg(feature = "mesh-llm")] but the initializer in app_state.rs was unconditional, so non-mesh builds tried to init a field that doesn't exist. I gated the initializer with #[cfg(feature = "mesh-llm")] and fully-qualified std::sync::atomic::AtomicU32 at the field + initializer so the AtomicU32 import is dropped (AtomicU16 is still imported and used, so no unused-import warning under -D warnings). No functional change to the mesh path. No need to take it over — pushed and ready for re-review.

The AppState field is #[cfg(feature = "mesh-llm")] but its initializer
was unconditional, breaking non-mesh builds under -D warnings. Gate the
initializer and fully-qualify AtomicU32 so the import is not left unused
(micspiral, block#2304).

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
@Bartok9
Bartok9 force-pushed the bartok9/mesh-rearm-on-dead-ingress branch from ec53e4a to 0edea64 Compare July 25, 2026 05:08
@Bartok9

Bartok9 commented Jul 25, 2026

Copy link
Copy Markdown
Author

Pushed 0edea6465 — same feature-gate fix, just re-signed off so DCO is green again. Ready for re-review.

Bartok9 added a commit to Bartok9/buzz that referenced this pull request Jul 25, 2026
micspiral block#2304 feature-gate left app_state.rs one line over the 1089
exception (trailing-newline count = 1090). Bump narrowly.
@Bartok9

Bartok9 commented Jul 25, 2026

Copy link
Copy Markdown
Author

@micspiral feature-gate was already on HEAD (0edea6465); Desktop Core still failed on the file-size ratchet (app_state.rs 1090 vs limit 1089 after the gate lines). Pushed a narrow +1 exception so non-mesh -D warnings stays fixed and desktop-check can pass. Thanks again for the precise diagnosis.

micspiral block#2304 feature-gate left app_state.rs one line over the 1089
exception (trailing-newline count = 1090). Bump narrowly.

Signed-off-by: Bartok9 <danielrpike9@gmail.com>
@Bartok9
Bartok9 force-pushed the bartok9/mesh-rearm-on-dead-ingress branch from f1f3f17 to df41532 Compare July 25, 2026 06:48
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