diff --git a/desktop/src-tauri/src/mesh_llm/snapshot.rs b/desktop/src-tauri/src/mesh_llm/snapshot.rs index 5b824d6b0..bc95d6d42 100644 --- a/desktop/src-tauri/src/mesh_llm/snapshot.rs +++ b/desktop/src-tauri/src/mesh_llm/snapshot.rs @@ -80,6 +80,13 @@ pub struct MeshSnapshot { pub devices: Vec, /// True when the local member's own device is among the sharing devices. pub includes_self: bool, + /// Members in the community right now (NIP-43 roster size). + /// + /// The denominator for "N of M sharing". Deliberately NOT used to estimate + /// unshared capacity: a member who never starts a node publishes no status + /// note, so their hardware is unknown by design — they never consented to + /// disclose it. Ghost nodes in the topology are a count, never GB. + pub member_count: usize, /// Why the snapshot is empty, when it is. Never a hard error: an empty /// mesh is a normal, expected state. pub reason: Option, @@ -238,6 +245,7 @@ pub fn snapshot_from_events( }; MeshSnapshot { + member_count: members.len(), sharing_device_count, shared_capacity_gb, models, diff --git a/desktop/src-tauri/src/mesh_llm/usage.rs b/desktop/src-tauri/src/mesh_llm/usage.rs index 830f224ef..56046cc71 100644 --- a/desktop/src-tauri/src/mesh_llm/usage.rs +++ b/desktop/src-tauri/src/mesh_llm/usage.rs @@ -29,6 +29,18 @@ pub struct MeshServingUsage { pub endpoint_attempts: u64, /// Other nodes currently visible as peers. pub peers: u64, + /// Completed requests this node's own GPU answered. + /// + /// From `routing_metrics.pressure`. Unlike the `*_attempts` counters these + /// count finished requests rather than tries, so they are the honest basis + /// for "this ran here" vs "this ran on someone else's machine". + pub locally_served: u64, + /// Completed requests a peer answered for this node — i.e. this machine + /// CONSUMED another member's compute. The one true "I used someone else's + /// hardware" figure available today. + pub remotely_served: u64, + /// Completed requests answered by a configured endpoint (not a mesh peer). + pub endpoint_served: u64, } /// Pure extractor: project a raw SDK status payload into [`MeshServingUsage`]. @@ -40,6 +52,7 @@ pub fn serving_usage_from_payload(payload: &serde_json::Value) -> MeshServingUsa let u64_at = |v: &serde_json::Value| v.as_u64().unwrap_or(0); let rm = payload.get("routing_metrics"); let local = rm.and_then(|m| m.get("local_node")); + let pressure = rm.and_then(|m| m.get("pressure")); let get_u64 = |obj: Option<&serde_json::Value>, key: &str| { obj.and_then(|o| o.get(key)).map(u64_at).unwrap_or(0) }; @@ -64,5 +77,8 @@ pub fn serving_usage_from_payload(payload: &serde_json::Value) -> MeshServingUsa .and_then(serde_json::Value::as_array) .map(|a| a.len() as u64) .unwrap_or(0), + locally_served: get_u64(pressure, "locally_served_request_count"), + remotely_served: get_u64(pressure, "remotely_served_request_count"), + endpoint_served: get_u64(pressure, "endpoint_request_count"), } } diff --git a/desktop/src/features/mesh-compute/meshCardModel.test.mjs b/desktop/src/features/mesh-compute/meshCardModel.test.mjs index ec97c30df..e7f7e3eac 100644 --- a/desktop/src/features/mesh-compute/meshCardModel.test.mjs +++ b/desktop/src/features/mesh-compute/meshCardModel.test.mjs @@ -105,7 +105,9 @@ test("an empty mesh reads as an honest empty state", () => { ), "No mesh capacity yet", ); - assert.equal(describeMeshCapacity(null), "No mesh capacity yet"); + // `null` is "not fetched yet" and must not be reported as an empty mesh: + // that would pass judgement on everyone else's machines during startup. + assert.equal(describeMeshCapacity(null), "Checking mesh capacity…"); }); test("capacity formatting keeps small figures meaningful", () => { diff --git a/desktop/src/features/mesh-compute/meshCardModel.ts b/desktop/src/features/mesh-compute/meshCardModel.ts index f2e14dd16..91406627e 100644 --- a/desktop/src/features/mesh-compute/meshCardModel.ts +++ b/desktop/src/features/mesh-compute/meshCardModel.ts @@ -76,7 +76,13 @@ function plural(n: number, one: string, many = `${one}s`): string { * says "total": the underlying query is capped at 100 members. */ export function describeMeshCapacity(snapshot: MeshSnapshot | null): string { - if (!snapshot || snapshot.sharingDeviceCount === 0) { + // `null` is "not fetched yet", NOT "the community has nothing". Claiming an + // empty mesh before the first snapshot lands reads as a verdict on everyone + // else's machines while the card is still starting up. + if (!snapshot) { + return "Checking mesh capacity…"; + } + if (snapshot.sharingDeviceCount === 0) { return "No mesh capacity yet"; } const { sharingDeviceCount: count, sharedCapacityGb: gb } = snapshot; @@ -280,7 +286,7 @@ export function deriveMeshCardModel({ ...base, tone: "idle", headline: capacity, - detail: ready ?? "Turn on to let members run agents on this computer.", + detail: ready ?? "Share compute to run models.", showSoloHint: false, }; } diff --git a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx index bf2d8e7c2..7419916cb 100644 --- a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx +++ b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx @@ -227,7 +227,7 @@ export function MeshComputeSettingsCard() {
{error ? ( diff --git a/desktop/src/shared/api/tauriMesh.ts b/desktop/src/shared/api/tauriMesh.ts index 53a4caeea..ea7e40c35 100644 --- a/desktop/src/shared/api/tauriMesh.ts +++ b/desktop/src/shared/api/tauriMesh.ts @@ -83,6 +83,22 @@ export type MeshServingUsage = { remoteAttempts: number; endpointAttempts: number; peers: number; + /** + * Completed requests this machine's own GPU answered. + * + * From `routing_metrics.pressure`. Unlike the `*Attempts` counters these are + * finished requests rather than tries, which makes them the honest basis for + * "this ran here" vs "this ran on someone else's machine". + */ + locallyServed: number; + /** + * Completed requests a peer answered for this machine — i.e. this machine + * CONSUMED another member's compute. The one true "I used someone else's + * hardware" figure available today. + */ + remotelyServed: number; + /** Completed requests answered by a configured endpoint, not a mesh peer. */ + endpointServed: number; }; export async function meshServingUsage(): Promise { @@ -156,6 +172,14 @@ export type MeshSnapshot = { models: string[]; devices: MeshSnapshotDevice[]; includesSelf: boolean; + /** + * Members in the community right now (NIP-43 roster size). + * + * The denominator for "N of M sharing". Never used to estimate unshared + * capacity: a member who never starts a node publishes no status note, so + * their hardware is unknown by design. Ghosts are a count, never GB. + */ + memberCount: number; reason: string | null; };