feat: add peer stats reporting (messages, tool calls, uptime, errors)
Peers self-report resource usage via set_stats; stats visible in list_peers responses and the new mesh_stats MCP tool. CLI auto-reports every 60s and tracks messagesIn/Out, toolCalls, uptime, and errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -264,6 +264,12 @@ Your message mode is "${messageMode}".
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
||||
const { name, arguments: args } = req.params;
|
||||
|
||||
// Track tool call count across all connected clients
|
||||
for (const c of allClients()) {
|
||||
c.incrementToolCalls();
|
||||
}
|
||||
|
||||
if (config.meshes.length === 0) {
|
||||
return text(
|
||||
"No meshes joined. Run `claudemesh join https://claudemesh.com/join/<token>` first.",
|
||||
@@ -913,6 +919,26 @@ Your message mode is "${messageMode}".
|
||||
return text(lines.join("\n"));
|
||||
}
|
||||
|
||||
case "mesh_stats": {
|
||||
const clients = allClients();
|
||||
if (clients.length === 0) return text("mesh_stats: no joined meshes", true);
|
||||
const sections: string[] = [];
|
||||
for (const c of clients) {
|
||||
const peers = await c.listPeers();
|
||||
const header = `## ${c.meshSlug}`;
|
||||
const rows = peers.map((p) => {
|
||||
const s = p.stats;
|
||||
if (!s) return `| ${p.displayName} | - | - | - | - | - |`;
|
||||
const up = s.uptime != null ? `${Math.floor(s.uptime / 60)}m` : "-";
|
||||
return `| ${p.displayName} | ${s.messagesIn ?? 0} | ${s.messagesOut ?? 0} | ${s.toolCalls ?? 0} | ${up} | ${s.errors ?? 0} |`;
|
||||
});
|
||||
sections.push(
|
||||
`${header}\n| Peer | Msgs In | Msgs Out | Tool Calls | Uptime | Errors |\n|------|---------|----------|------------|--------|--------|\n${rows.join("\n")}`,
|
||||
);
|
||||
}
|
||||
return text(sections.join("\n\n"));
|
||||
}
|
||||
|
||||
case "ping_mesh": {
|
||||
const { priorities: pingPriorities } = (args ?? {}) as { priorities?: string[] };
|
||||
const toTest = (pingPriorities ?? ["now", "next"]) as Priority[];
|
||||
|
||||
@@ -609,6 +609,14 @@ export const TOOLS: Tool[] = [
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
|
||||
// --- Stats ---
|
||||
{
|
||||
name: "mesh_stats",
|
||||
description:
|
||||
"View resource usage stats for all peers: messages sent/received, tool calls, uptime, errors.",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
|
||||
// --- MCP Proxy ---
|
||||
{
|
||||
name: "mesh_mcp_register",
|
||||
@@ -669,6 +677,42 @@ export const TOOLS: Tool[] = [
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// --- Simulation clock tools ---
|
||||
{
|
||||
name: "mesh_set_clock",
|
||||
description:
|
||||
"Set the simulation clock speed. x1 = real-time, x10 = 10x faster, x100 = 100x. Peers receive heartbeat ticks at the simulated rate.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
speed: {
|
||||
type: "number",
|
||||
description: "Speed multiplier (1-100). x1 = tick every 60s, x10 = tick every 6s, x100 = tick every 600ms.",
|
||||
},
|
||||
},
|
||||
required: ["speed"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mesh_pause_clock",
|
||||
description:
|
||||
"Pause the simulation clock. Ticks stop until resumed.",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
{
|
||||
name: "mesh_resume_clock",
|
||||
description:
|
||||
"Resume a paused simulation clock.",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
{
|
||||
name: "mesh_clock",
|
||||
description:
|
||||
"Get current simulation clock status: speed, tick count, simulated time.",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
|
||||
// --- Diagnostics ---
|
||||
{
|
||||
name: "ping_mesh",
|
||||
|
||||
@@ -45,6 +45,13 @@ export interface PeerInfo {
|
||||
uptime?: number;
|
||||
errors?: number;
|
||||
};
|
||||
visible?: boolean;
|
||||
profile?: {
|
||||
avatar?: string;
|
||||
title?: string;
|
||||
bio?: string;
|
||||
capabilities?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface InboundPush {
|
||||
@@ -219,6 +226,7 @@ export class BrokerClient {
|
||||
this.setConnStatus("open");
|
||||
this.reconnectAttempt = 0;
|
||||
this.flushOutbound();
|
||||
this.startStatsReporting();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
@@ -271,6 +279,8 @@ export class BrokerClient {
|
||||
ciphertext = Buffer.from(message, "utf-8").toString("base64");
|
||||
}
|
||||
|
||||
this._statsCounters.messagesOut++;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
if (this.pendingSends.size >= MAX_QUEUED) {
|
||||
resolve({ ok: false, error: "outbound queue full" });
|
||||
@@ -941,8 +951,57 @@ export class BrokerClient {
|
||||
|
||||
// --- Mesh info ---
|
||||
private meshInfoResolvers = new Map<string, { resolve: (result: Record<string, unknown> | null) => void; timer: NodeJS.Timeout }>();
|
||||
private clockStatusResolvers = new Map<string, { resolve: (result: { speed: number; paused: boolean; tick: number; simTime: string; startedAt: string } | null) => void; timer: NodeJS.Timeout }>();
|
||||
|
||||
async meshInfo(): Promise<Record<string, unknown> | null> {
|
||||
/** Set the simulation clock speed. Returns clock status. */
|
||||
async setClock(speed: number): Promise<{ speed: number; paused: boolean; tick: number; simTime: string; startedAt: string } | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
|
||||
if (this.clockStatusResolvers.delete(reqId)) resolve(null);
|
||||
}, 5_000) });
|
||||
this.ws!.send(JSON.stringify({ type: "set_clock", speed, _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
/** Pause the simulation clock. Returns clock status. */
|
||||
async pauseClock(): Promise<{ speed: number; paused: boolean; tick: number; simTime: string; startedAt: string } | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
|
||||
if (this.clockStatusResolvers.delete(reqId)) resolve(null);
|
||||
}, 5_000) });
|
||||
this.ws!.send(JSON.stringify({ type: "pause_clock", _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
/** Resume the simulation clock. Returns clock status. */
|
||||
async resumeClock(): Promise<{ speed: number; paused: boolean; tick: number; simTime: string; startedAt: string } | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
|
||||
if (this.clockStatusResolvers.delete(reqId)) resolve(null);
|
||||
}, 5_000) });
|
||||
this.ws!.send(JSON.stringify({ type: "resume_clock", _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
/** Get current simulation clock status. */
|
||||
async getClock(): Promise<{ speed: number; paused: boolean; tick: number; simTime: string; startedAt: string } | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.clockStatusResolvers.set(reqId, { resolve, timer: setTimeout(() => {
|
||||
if (this.clockStatusResolvers.delete(reqId)) resolve(null);
|
||||
}, 5_000) });
|
||||
this.ws!.send(JSON.stringify({ type: "get_clock", _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
async meshInfo(): Promise<Record<string, unknown> | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
@@ -953,8 +1012,66 @@ export class BrokerClient {
|
||||
});
|
||||
}
|
||||
|
||||
// --- Skills ---
|
||||
private skillAckResolvers = new Map<string, { resolve: (result: { name: string; action: string } | null) => void; timer: NodeJS.Timeout }>();
|
||||
private skillDataResolvers = new Map<string, { resolve: (skill: { name: string; description: string; instructions: string; tags: string[]; author: string; createdAt: string } | null) => void; timer: NodeJS.Timeout }>();
|
||||
private skillListResolvers = new Map<string, { resolve: (skills: Array<{ name: string; description: string; tags: string[]; author: string; createdAt: string }>) => void; timer: NodeJS.Timeout }>();
|
||||
|
||||
/** Publish a reusable skill to the mesh. */
|
||||
async shareSkill(name: string, description: string, instructions: string, tags?: string[]): Promise<{ ok: boolean; action?: string } | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.skillAckResolvers.set(reqId, { resolve: (result) => {
|
||||
resolve(result ? { ok: true, action: result.action } : null);
|
||||
}, timer: setTimeout(() => {
|
||||
if (this.skillAckResolvers.delete(reqId)) resolve(null);
|
||||
}, 5_000) });
|
||||
this.ws!.send(JSON.stringify({ type: "share_skill", name, description, instructions, tags, _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
/** Load a skill's full instructions by name. */
|
||||
async getSkill(name: string): Promise<{ name: string; description: string; instructions: string; tags: string[]; author: string; createdAt: string } | null> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return null;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.skillDataResolvers.set(reqId, { resolve, timer: setTimeout(() => {
|
||||
if (this.skillDataResolvers.delete(reqId)) resolve(null);
|
||||
}, 5_000) });
|
||||
this.ws!.send(JSON.stringify({ type: "get_skill", name, _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
/** Browse available skills in the mesh. */
|
||||
async listSkills(query?: string): Promise<Array<{ name: string; description: string; tags: string[]; author: string; createdAt: string }>> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return [];
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.skillListResolvers.set(reqId, { resolve, timer: setTimeout(() => {
|
||||
if (this.skillListResolvers.delete(reqId)) resolve([]);
|
||||
}, 5_000) });
|
||||
this.ws!.send(JSON.stringify({ type: "list_skills", query, _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
/** Remove a skill you published. */
|
||||
async removeSkill(name: string): Promise<boolean> {
|
||||
if (!this.ws || this.ws.readyState !== this.ws.OPEN) return false;
|
||||
return new Promise((resolve) => {
|
||||
const reqId = this.makeReqId();
|
||||
this.skillAckResolvers.set(reqId, { resolve: (result) => {
|
||||
resolve(result?.action === "removed");
|
||||
}, timer: setTimeout(() => {
|
||||
if (this.skillAckResolvers.delete(reqId)) resolve(false);
|
||||
}, 5_000) });
|
||||
this.ws!.send(JSON.stringify({ type: "remove_skill", name, _reqId: reqId }));
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.closed = true;
|
||||
this.stopStatsReporting();
|
||||
if (this.helloTimer) clearTimeout(this.helloTimer);
|
||||
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
||||
if (this.ws) {
|
||||
@@ -1013,6 +1130,7 @@ export class BrokerClient {
|
||||
return;
|
||||
}
|
||||
if (msg.type === "push") {
|
||||
this._statsCounters.messagesIn++;
|
||||
const nonce = String(msg.nonce ?? "");
|
||||
const ciphertext = String(msg.ciphertext ?? "");
|
||||
const senderPubkey = String(msg.senderPubkey ?? "");
|
||||
@@ -1234,6 +1352,16 @@ export class BrokerClient {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (msg.type === "clock_status") {
|
||||
this.resolveFromMap(this.clockStatusResolvers, msgReqId, {
|
||||
speed: Number(msg.speed ?? 0),
|
||||
paused: Boolean(msg.paused),
|
||||
tick: Number(msg.tick ?? 0),
|
||||
simTime: String(msg.simTime ?? ""),
|
||||
startedAt: String(msg.startedAt ?? ""),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (msg.type === "mesh_info_result") {
|
||||
this.resolveFromMap(this.meshInfoResolvers, msgReqId, msg as Record<string, unknown>);
|
||||
return;
|
||||
@@ -1337,6 +1465,7 @@ export class BrokerClient {
|
||||
[this.streamCreatedResolvers, null],
|
||||
[this.listPeersResolvers, []],
|
||||
[this.meshInfoResolvers, null],
|
||||
[this.clockStatusResolvers, null],
|
||||
[this.mcpRegisterResolvers, null],
|
||||
[this.mcpListResolvers, []],
|
||||
[this.mcpCallResolvers, { error: "broker error" }],
|
||||
|
||||
Reference in New Issue
Block a user