Skip to content

fix(desktop): shared-compute usability — share toggle, usage indicator, model resync#2448

Merged
micspiral merged 12 commits into
mainfrom
micn/mesh-state-fix
Jul 24, 2026
Merged

fix(desktop): shared-compute usability — share toggle, usage indicator, model resync#2448
micspiral merged 12 commits into
mainfrom
micn/mesh-state-fix

Conversation

@micspiral

@micspiral micspiral commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator
image

Shared-compute usability fixes, discovered while getting a local agent to reliably run and reply on Buzz shared compute. Three related but distinct changes:

1. Share toggle reflects serve mode, not slot occupancy

  • Serve (sharing) and consume (client) share one runtime slot, and both report running — so consuming a peer's compute wrongly lit the Share toggle.
  • Toggle is now mode-aware: serve = sharing (on), client = consuming (off, disabled with an explanatory line).
  • Backend guard: mesh_stop_node refuses to tear down a client node, so a stale UI can't kill a consume session.
  • A failed serve node still occupies the slot, so it reads as sharing and stays turn-off-able (no dead-end).
  • Pure predicate + serde-contract + lifecycle tests; e2e regression proven red→green.

2. Surface who is using shared compute on the Share card

  • Read-only mesh_serving_usage command projects the node's own routing_metrics (inflight, tokens served, tok/s, and the local-vs-remote/endpoint split that distinguishes your agent from a peer consuming your compute).
  • Small indicator under the Share status line: idle / serving your agent / in use by another member.
  • No new trust surface — just exposes what the runtime already computes. Stubbed for non-mesh builds. Unit + e2e coverage.

3. Resync the Share model field when the served model changes

  • The card mirrored the node's modelId into the field only when the field was empty, so switching models (node restarts on the new one) left a stale model shown.
  • Now syncs whenever the served model differs from the field. Safe from clobbering a mid-edit because the field is disabled while the slot is occupied.

Testing

  • Rust: predicate/serde/lifecycle unit tests (all green); clippy + fmt clean.
  • Frontend: predicate + serving-usage unit tests, e2e regression specs.
  • Manual: full serve lifecycle (on → serve/route agent → clean stop → cache-backed restart) on the meshllm community.

Note: This PR originally also included a buzz-acp content-delivery fallback
(posting a plain-text reply when a weak model answers in prose and never calls
the send tool). That change has been split out into its own PR — #2681 — so it
can be reviewed independently of these shared-compute usability fixes, and has
been reverted out of this branch.

Dependency bump

Pins the six mesh-llm crates (sdk, host-runtime, client, node,
system, events) from tag v0.73.1 → rev c441ea7 (v0.73.1 + 1 commit) —
the patched mesh build these shared-compute fixes were developed and validated
against.

Consuming a peer's compute (selecting Buzz compute as an agent's
provider) spins up a client-mode node in the single mesh runtime slot,
which reports state:"running". The Share toggle keyed off state alone,
so it lit up during a consume session — and toggling it off ran the
generic mesh_stop_node, tearing down that session.

Toggle now reads mode: serve = sharing (on), client = consuming (off).
A serve node that also routes a peer's model stays on. mesh_stop_node
refuses to tear down a client node.

A failed serve node still occupies the single slot, so it reads as
sharing (switch stays on and turn-off-able to clear/retry) and blocks a
fresh start; slotOccupied gates the model inputs. Consume copy states
the fact rather than promising a stop action that doesn't exist.

- deriveMeshShareToggle predicate (+ slotOccupied) + unit table
- backend share_stop_should_teardown guard + serde contract test
- e2e regression: consuming does not light the Share toggle
@micspiral
micspiral marked this pull request as ready for review July 23, 2026 01:29
@micspiral
micspiral requested a review from a team as a code owner July 23, 2026 01:29
@micspiral
micspiral enabled auto-merge (squash) July 23, 2026 01:30
Adds a read-only, host-side usage indicator to the Share compute card so
you can see when someone (or your own agent) is using the compute this
machine is sharing, and where it's coming from.

- new read-only mesh_serving_usage Tauri command projects the serving
  node's own routing_metrics (request/token counts, inflight, and the
  local vs remote/endpoint attempt split). No new trust surface; stubbed
  for non-mesh-llm builds.
- serving_usage_from_payload: pure, defensive extractor (missing fields
  -> 0, so SDK shape drift degrades to 'no usage' not an error).
- deriveServingIndicator: pure label derivation. Distinguishes this
  machine's OWN agents (local, shown softly) from another member
  consuming this machine's compute (remote/endpoint -> the headline
  'in use by another member' case).
- useMeshServingUsage: 4s poll, only while sharing.
- indicator rendered under the Share toggle status line.
- tests: 3 Rust (extractor local/remote/default), 7 TS (indicator label
  table), e2e mock answers mesh_serving_usage + seed hook.

Verified live: firing a completion at the mesh API moves exactly the
fields the extractor reads (req/tokens/local_attempt), local-only traffic
correctly reads as not-a-remote-consumer.

Local-only exploration on the mesh-state-fix branch; not part of PR #2448.
The card mirrored the running node's modelId into the field only when the
field was empty, so switching models (node restarts on the new one) left the
field showing the stale model. Sync whenever the served model differs from the
field; safe from clobbering a mid-edit because the field is disabled while the
slot is occupied.
buzz-agent's output is its tool calls; streamed assistant content is
observability-only and normally never posted. Capable models reliably call
`buzz messages send`, but weaker local models (e.g. via Buzz shared compute)
often answer a conversational prompt in plain content and never call the send
tool, silently dropping the reply.

Add a content-delivery fallback in buzz-acp: track per-turn whether the model
published its own message and buffer streamed content; on a normal turn end
with content but no publish tool call, post that content as a threaded kind-9
reply (reusing buzz_sdk::build_message, signed with the agent keys, best-effort,
mirroring post_failure_notice). Skips bare acknowledgements the base prompt
forbids publishing.

Live-validated against a small Gemma on a real community: the fallback fires
when the model answers in prose and stays dormant when it calls the send tool.

Unit tests cover send-tool detection (incl. read-vs-send discrimination) and
bare-ack filtering.
@micspiral micspiral changed the title fix(desktop): Share toggle reflects serve mode, not slot occupancy fix(desktop,acp): shared-compute usability — toggle, usage indicator, model resync, reply delivery Jul 23, 2026
Apply desktop Tauri rustfmt to mod_tests.rs — the desktop crate has a separate
fmt gate (desktop-tauri-fmt-check) that the workspace fmt does not cover.
… 429

When a Buzz-compute agent starts, wait_for_mesh_inference polled the local
mesh ingress for up to 120s and, on failure, surfaced a raw "HTTP 429: model
not currently available" while the node still reported Running/Ok. In the GUI
this reads as a frozen dialog that eventually dies with a meaningless error —
the observed "consume never comes up / every model select fails" symptom.

Cross-machine testing (M5 serve; mini + M4 consume; native mesh-llm control)
showed two physically distinct readiness failures:
  * catalog never syncs — the client connects to the host at the control
    level (ping/RTT fine) but the peer gossip stream never establishes across
    the network (iroh MultipathNotNegotiated / unreachable direct path), so
    the served model never enters the local /v1/models catalog and every
    request is rejected. Root cause is the network path to the host.
  * routing never completes — the model syncs into the catalog but per-request
    routing/transport to the host keeps failing.

wait_for_mesh_inference now tracks whether the served model was ever visible in
the local catalog during the wait and, on timeout, returns a stage-specific,
actionable message via a pure classifier (classify_mesh_readiness_failure) and
message builder (mesh_readiness_failure_message), both unit-tested. This does
not fix the underlying transport limitation (which lives in the mesh-llm/iroh
SDK, and reproduces with the native mesh-llm client too) — it makes the failure
legible instead of a silent 120s wall.

Note: the cross-machine consume path is NOT yet reliably working; this commit
is the user-facing diagnosis mitigation only.
@micspiral
micspiral disabled auto-merge July 23, 2026 16:19
@micspiral

Copy link
Copy Markdown
Collaborator Author

almost there....

@micspiral

Copy link
Copy Markdown
Collaborator Author

one missing piece sometimes blocks the connection... standby...

Repins the mesh-llm-* git deps from tag v0.73.1 to
rev c441ea7f328692b18b7f49ad819c7b1a603cbdcb, the v0.73.1-based backport of the
owner-control / main-mesh relay endpoint-id collision fix
(Mesh-LLM/mesh-llm#1064).

Without the fix, a consumer with an owner identity (buzz always sets one for
admission) starts a second iroh endpoint sharing the node's endpoint id; that
endpoint evicts the main mesh endpoint's relay registration, silently killing
relay fallback. Any consumer that cannot reach the host directly then never
joins and every inference 429/503s. The rev still reports version 0.73.1, so the
native runtime download resolves to the existing v0.73.1 release unchanged.

Hand-verified in the release app on the meshllm community: mini consumed M5's
shared gemma with owner identity present on both sides (the exact pre-fix
failure condition), inference returned 200.

NOTE: interim pin to an unmerged rev; retarget to a v0.73.x tag once #1064 merges.
@micspiral

Copy link
Copy Markdown
Collaborator Author

OK this is now confirmed to work end to end. THere will be more holepiunch improvements coming, they dont need to block this however, as this will unblock people.

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Reviewed at head e07638b (all CI green; cargo test -p buzz-acp 594/0 locally at exact head).

Parts 1–3 (toggle, usage indicator, model resync): clean, ship-ready. The deriveMeshShareToggle predicate + authoritative backend guard in mesh_stop_node (mode check under the lock, client node left untouched, config save skipped) handle the failed-serve-occupies-slot nuance correctly, and the e2e regression pins it. Serving usage is a genuinely read-only projection with defensive extraction. Model resync can't clobber a mid-edit (field disabled while slot occupied) and can't loop.

Part 4 (content-delivery fallback): right concept, but the publish detection is too narrow, and the miss direction is user-visible double-posting.

tool_call_is_message_publish substring-matches "messages send" in title+rawInput. A common publish pattern — the one agents are taught for content containing backticks — is Python argv: subprocess.run(['buzz','messages','send',...]). Serialized rawInput contains 'buzz','messages','send' with no "messages send" substring → detection misses. Same for wrapper scripts. Consequence: a capable model that narrates between tool calls (streamed agent_message_chunks) and then publishes via argv gets turn_sent_message=false, and the fallback posts the entire concatenated turn narration as a second channel reply. That's every desktop-managed agent on this harness that publishes this way, not an edge case.

Related: a deliberate-silence turn (base prompt: "silence is usually correct") that streamed any assistant text gets that text force-posted; the ≤40-char bare-ack list doesn't cover, e.g., "Nothing new here, ending the turn."

Two fixes, either sufficient, both better together:

  1. Broaden detection — normalize the haystack (strip quotes/commas/brackets, or token-match buzz + messages + send), and/or detect from tool_call_update output: the send response envelope {"accepted":true,"event_id":...} is a much stronger signal than the input.
  2. Scope-gate the fallback to sessions on local/mesh model profiles (the PR's own motivation), or behind a config flag — eliminates the capable-model blast radius entirely.

Minor:

  • The mesh-llm pin moves tag v0.73.1 → rev c441ea7 (= v0.73.1 + 1 commit, "owner-control listener must not share the mesh relay slot"). Worth naming in the PR body — it's an undisclosed dep bump.
  • Detection counts a failed send (nonzero exit) as delivered — fail-safe vs the status quo, fine.

Mechanics of the fallback are otherwise correct: EndTurn-only, take-once semantics mean no double-fire across the two EndTurn arms, and cancelled/truncated turns can't leak partial text.

Verdict: parts 1–3 mergeable today; part 4 needs the detection fix or the scope gate first.

@micspiral

Copy link
Copy Markdown
Collaborator Author

thanks @tlongwell-block - on 4 now. and yeah it will go to a released version in future for sure (which will have may other fixes and split model support for frontier class, another day).

(writing by hand old school)

npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 2 commits July 23, 2026 21:09
Review findings from #2448 (Wren + Eva): the content-delivery fallback
inferred delivery from the send tool call's INPUT, which was wrong in
both directions:

- False negative (silent drop): turn_sent_message was set the moment a
  tool_call's rawInput matched 'messages send' — intent, not delivery.
  A send that subsequently failed (relay/network/auth/CLI error) still
  suppressed the fallback, dropping the exact reply the feature exists
  to save.
- False positive (double post): the substring match missed argv-style
  publishes (subprocess.run(['buzz','messages','send',...])), so a
  capable model that narrates between tool calls and publishes via
  argv would get its whole turn narration re-posted by the fallback.

Fix: registering a publish is now two-phase.
- tool_call input matching (normalized so shell strings AND argv forms
  match) only registers a CANDIDATE toolCallId.
- Delivery is confirmed solely from the candidate's terminal
  tool_call_update outcome: failed/cancelled → not delivered (fallback
  stays armed); completed → inspect rawOutput.isError, the CLI's
  {"accepted":...} response envelope, and any reported exit_code;
  a bare completed with no inspectable output keeps the status-quo
  direction (suppress, i.e. today's behavior).

Tests: outcome classifier matrix (pending/failed/cancelled/completed x
envelope/isError/exit_code) plus handler-level lifecycle tests through
handle_session_update + take_undelivered_turn_message covering success,
failure, cancel, failed-then-retry, and argv-style publish.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
* origin/main:
  Clarify agent harness defaults in create flow (#2601)
  chore(mobile): relax release check (#2636)

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@micspiral

Copy link
Copy Markdown
Collaborator Author

@tlongwell-block this looks good to me now - I am not happy with any approach I have found for scoping it yet, so one of those sufficient fixes I think makes it worth it, so up to you if/when to include it (am having an agent have oner more look)

@micspiral micspiral changed the title fix(desktop,acp): shared-compute usability — toggle, usage indicator, model resync, reply delivery fix(desktop): shared-compute usability — share toggle, usage indicator, model resync Jul 24, 2026
@micspiral

Copy link
Copy Markdown
Collaborator Author

@tlongwell-block decoupled the prose capture into PR #2681 to get it out of this - hope that makes sense

@micspiral
micspiral merged commit 9cc9652 into main Jul 24, 2026
25 checks passed
@micspiral
micspiral deleted the micn/mesh-state-fix branch July 24, 2026 06:21
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