@sleep2agi/commhub-server 0.8.1-preview.3 → 0.8.1-preview.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/db.ts +56 -2
- package/src/index.ts +211 -0
- package/src/tools.ts +34 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.8.1-preview.
|
|
3
|
+
"version": "0.8.1-preview.4",
|
|
4
4
|
"description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/db.ts
CHANGED
|
@@ -24,6 +24,13 @@ db.exec(`
|
|
|
24
24
|
mem_total_gb REAL,
|
|
25
25
|
mem_used_gb REAL,
|
|
26
26
|
mem_avail_gb REAL,
|
|
27
|
+
disk_total_gb REAL,
|
|
28
|
+
disk_used_gb REAL,
|
|
29
|
+
disk_avail_gb REAL,
|
|
30
|
+
process_rss_bytes INTEGER,
|
|
31
|
+
process_cpu_pct REAL,
|
|
32
|
+
process_uptime_seconds REAL,
|
|
33
|
+
process_in_flight_count INTEGER,
|
|
27
34
|
network_id TEXT NOT NULL DEFAULT 'default',
|
|
28
35
|
registered_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
29
36
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
@@ -73,10 +80,46 @@ for (const col of [
|
|
|
73
80
|
{ name: "mem_total_gb", def: "REAL" },
|
|
74
81
|
{ name: "mem_used_gb", def: "REAL" },
|
|
75
82
|
{ name: "mem_avail_gb", def: "REAL" },
|
|
83
|
+
{ name: "disk_total_gb", def: "REAL" },
|
|
84
|
+
{ name: "disk_used_gb", def: "REAL" },
|
|
85
|
+
{ name: "disk_avail_gb", def: "REAL" },
|
|
86
|
+
{ name: "process_rss_bytes", def: "INTEGER" },
|
|
87
|
+
{ name: "process_cpu_pct", def: "REAL" },
|
|
88
|
+
{ name: "process_uptime_seconds", def: "REAL" },
|
|
89
|
+
{ name: "process_in_flight_count", def: "INTEGER" },
|
|
76
90
|
]) {
|
|
77
91
|
try { db.exec(`ALTER TABLE sessions ADD COLUMN ${col.name} ${col.def}`); } catch {}
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
db.exec(`
|
|
95
|
+
CREATE TABLE IF NOT EXISTS agent_telemetry (
|
|
96
|
+
id TEXT PRIMARY KEY,
|
|
97
|
+
network_id TEXT NOT NULL DEFAULT 'default',
|
|
98
|
+
resume_id TEXT,
|
|
99
|
+
alias TEXT,
|
|
100
|
+
hostname TEXT,
|
|
101
|
+
ip TEXT,
|
|
102
|
+
cpu_load_1min REAL,
|
|
103
|
+
cpu_cores INTEGER,
|
|
104
|
+
mem_total_gb REAL,
|
|
105
|
+
mem_used_gb REAL,
|
|
106
|
+
mem_avail_gb REAL,
|
|
107
|
+
disk_total_gb REAL,
|
|
108
|
+
disk_used_gb REAL,
|
|
109
|
+
disk_avail_gb REAL,
|
|
110
|
+
process_rss_bytes INTEGER,
|
|
111
|
+
process_cpu_pct REAL,
|
|
112
|
+
process_uptime_seconds REAL,
|
|
113
|
+
process_in_flight_count INTEGER,
|
|
114
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_agent_telemetry_host_time
|
|
118
|
+
ON agent_telemetry(network_id, hostname, ip, created_at);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_agent_telemetry_alias_time
|
|
120
|
+
ON agent_telemetry(network_id, alias, created_at);
|
|
121
|
+
`);
|
|
122
|
+
|
|
80
123
|
// inbox: add in_reply_to, requires_response, expires_at, scope
|
|
81
124
|
for (const col of [
|
|
82
125
|
{ name: "in_reply_to", def: "TEXT" },
|
|
@@ -392,6 +435,13 @@ function migrateSessionsNetworkAliasUnique() {
|
|
|
392
435
|
mem_total_gb REAL,
|
|
393
436
|
mem_used_gb REAL,
|
|
394
437
|
mem_avail_gb REAL,
|
|
438
|
+
disk_total_gb REAL,
|
|
439
|
+
disk_used_gb REAL,
|
|
440
|
+
disk_avail_gb REAL,
|
|
441
|
+
process_rss_bytes INTEGER,
|
|
442
|
+
process_cpu_pct REAL,
|
|
443
|
+
process_uptime_seconds REAL,
|
|
444
|
+
process_in_flight_count INTEGER,
|
|
395
445
|
network_id TEXT NOT NULL DEFAULT 'default',
|
|
396
446
|
UNIQUE (network_id, alias)
|
|
397
447
|
)
|
|
@@ -401,13 +451,17 @@ function migrateSessionsNetworkAliasUnique() {
|
|
|
401
451
|
resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version,
|
|
402
452
|
status, task, output, progress, score, registered_at, updated_at, node_id,
|
|
403
453
|
session_id, config_path, channels, last_seen_at, model, cpu_load_1min,
|
|
404
|
-
cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb,
|
|
454
|
+
cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb,
|
|
455
|
+
disk_used_gb, disk_avail_gb, process_rss_bytes, process_cpu_pct,
|
|
456
|
+
process_uptime_seconds, process_in_flight_count, network_id
|
|
405
457
|
)
|
|
406
458
|
SELECT
|
|
407
459
|
resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version,
|
|
408
460
|
status, task, output, progress, score, registered_at, updated_at, node_id,
|
|
409
461
|
session_id, config_path, channels, last_seen_at, model, cpu_load_1min,
|
|
410
|
-
cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb,
|
|
462
|
+
cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb,
|
|
463
|
+
disk_used_gb, disk_avail_gb, process_rss_bytes, process_cpu_pct,
|
|
464
|
+
process_uptime_seconds, process_in_flight_count,
|
|
411
465
|
COALESCE(NULLIF(network_id, ''), 'default')
|
|
412
466
|
FROM sessions
|
|
413
467
|
ORDER BY updated_at
|
package/src/index.ts
CHANGED
|
@@ -229,6 +229,104 @@ function singleNetworkId(scope: RestNetworkScope): string | null {
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
function sqliteTime(date: Date): string {
|
|
233
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parseSqliteTime(value: string | null | undefined): number {
|
|
237
|
+
if (!value) return 0;
|
|
238
|
+
const normalized = value.includes("T") ? value : `${value.replace(" ", "T")}Z`;
|
|
239
|
+
const ts = Date.parse(normalized);
|
|
240
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function cpuPct(load: number | null | undefined, cores: number | null | undefined): number | null {
|
|
244
|
+
if (typeof load !== "number" || typeof cores !== "number" || cores <= 0) return null;
|
|
245
|
+
return Math.round((load / cores) * 1000) / 10;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function serverAlertLevel(row: any): { level: "green" | "yellow" | "red"; alerts: string[] } {
|
|
249
|
+
const alerts: string[] = [];
|
|
250
|
+
const pct = cpuPct(row?.cpu_load_1min, row?.cpu_cores);
|
|
251
|
+
if (pct !== null && pct >= 80) alerts.push(`cpu ${pct}%`);
|
|
252
|
+
if (typeof row?.mem_avail_gb === "number" && row.mem_avail_gb < 0.5) alerts.push(`memory ${row.mem_avail_gb}GB available`);
|
|
253
|
+
if (typeof row?.disk_avail_gb === "number" && row.disk_avail_gb < 1) alerts.push(`disk ${row.disk_avail_gb}GB available`);
|
|
254
|
+
if (alerts.length > 0) return { level: "red", alerts };
|
|
255
|
+
|
|
256
|
+
if (pct !== null && pct >= 60) alerts.push(`cpu ${pct}%`);
|
|
257
|
+
if (typeof row?.mem_avail_gb === "number" && row.mem_avail_gb < 1) alerts.push(`memory ${row.mem_avail_gb}GB available`);
|
|
258
|
+
if (typeof row?.disk_avail_gb === "number" && row.disk_avail_gb < 5) alerts.push(`disk ${row.disk_avail_gb}GB available`);
|
|
259
|
+
return { level: alerts.length > 0 ? "yellow" : "green", alerts };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function agentHealthChip(status: unknown, lastSeen: string | null | undefined): "online" | "offline" | "stale" {
|
|
263
|
+
if (String(status || "").toLowerCase() === "offline") return "offline";
|
|
264
|
+
const ts = parseSqliteTime(lastSeen);
|
|
265
|
+
if (!ts || Date.now() - ts > 5 * 60 * 1000) return "stale";
|
|
266
|
+
return "online";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function bucketTelemetry(rows: any[], fromMs: number, bucketMs: number) {
|
|
270
|
+
const buckets = new Map<number, {
|
|
271
|
+
ts: number;
|
|
272
|
+
count: number;
|
|
273
|
+
cpu_pct_sum: number;
|
|
274
|
+
cpu_pct_count: number;
|
|
275
|
+
cpu_load_sum: number;
|
|
276
|
+
cpu_load_count: number;
|
|
277
|
+
mem_avail_min: number | null;
|
|
278
|
+
mem_used_max: number | null;
|
|
279
|
+
disk_avail_min: number | null;
|
|
280
|
+
disk_used_max: number | null;
|
|
281
|
+
}>();
|
|
282
|
+
|
|
283
|
+
for (const row of rows) {
|
|
284
|
+
const ts = parseSqliteTime(row.created_at);
|
|
285
|
+
if (!ts || ts < fromMs) continue;
|
|
286
|
+
const bucketTs = Math.floor(ts / bucketMs) * bucketMs;
|
|
287
|
+
const bucket = buckets.get(bucketTs) ?? {
|
|
288
|
+
ts: bucketTs,
|
|
289
|
+
count: 0,
|
|
290
|
+
cpu_pct_sum: 0,
|
|
291
|
+
cpu_pct_count: 0,
|
|
292
|
+
cpu_load_sum: 0,
|
|
293
|
+
cpu_load_count: 0,
|
|
294
|
+
mem_avail_min: null,
|
|
295
|
+
mem_used_max: null,
|
|
296
|
+
disk_avail_min: null,
|
|
297
|
+
disk_used_max: null,
|
|
298
|
+
};
|
|
299
|
+
bucket.count += 1;
|
|
300
|
+
const pct = cpuPct(row.cpu_load_1min, row.cpu_cores);
|
|
301
|
+
if (pct !== null) {
|
|
302
|
+
bucket.cpu_pct_sum += pct;
|
|
303
|
+
bucket.cpu_pct_count += 1;
|
|
304
|
+
}
|
|
305
|
+
if (typeof row.cpu_load_1min === "number") {
|
|
306
|
+
bucket.cpu_load_sum += row.cpu_load_1min;
|
|
307
|
+
bucket.cpu_load_count += 1;
|
|
308
|
+
}
|
|
309
|
+
if (typeof row.mem_avail_gb === "number") bucket.mem_avail_min = bucket.mem_avail_min === null ? row.mem_avail_gb : Math.min(bucket.mem_avail_min, row.mem_avail_gb);
|
|
310
|
+
if (typeof row.mem_used_gb === "number") bucket.mem_used_max = bucket.mem_used_max === null ? row.mem_used_gb : Math.max(bucket.mem_used_max, row.mem_used_gb);
|
|
311
|
+
if (typeof row.disk_avail_gb === "number") bucket.disk_avail_min = bucket.disk_avail_min === null ? row.disk_avail_gb : Math.min(bucket.disk_avail_min, row.disk_avail_gb);
|
|
312
|
+
if (typeof row.disk_used_gb === "number") bucket.disk_used_max = bucket.disk_used_max === null ? row.disk_used_gb : Math.max(bucket.disk_used_max, row.disk_used_gb);
|
|
313
|
+
buckets.set(bucketTs, bucket);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return Array.from(buckets.values())
|
|
317
|
+
.sort((a, b) => a.ts - b.ts)
|
|
318
|
+
.map((b) => ({
|
|
319
|
+
ts: new Date(b.ts).toISOString(),
|
|
320
|
+
count: b.count,
|
|
321
|
+
cpu_pct: b.cpu_pct_count ? Math.round((b.cpu_pct_sum / b.cpu_pct_count) * 10) / 10 : null,
|
|
322
|
+
cpu_load_1min: b.cpu_load_count ? Math.round((b.cpu_load_sum / b.cpu_load_count) * 100) / 100 : null,
|
|
323
|
+
mem_avail_gb: b.mem_avail_min,
|
|
324
|
+
mem_used_gb: b.mem_used_max,
|
|
325
|
+
disk_avail_gb: b.disk_avail_min,
|
|
326
|
+
disk_used_gb: b.disk_used_max,
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
|
|
232
330
|
function canRestWriteNetwork(authCtx: { userId: string; networkId: string | null } | null, networkId: string | null, isAdmin: boolean): boolean {
|
|
233
331
|
if (!authCtx) return true; // legacy global token or open dev mode
|
|
234
332
|
if (isAdmin) return true;
|
|
@@ -894,6 +992,119 @@ Bun.serve({
|
|
|
894
992
|
return withCors(req, Response.json(Array.from(grouped.values())));
|
|
895
993
|
}
|
|
896
994
|
|
|
995
|
+
const serverDetailMatch = url.pathname.match(/^\/api\/server\/([^/]+)\/(health|agents)$/);
|
|
996
|
+
if (serverDetailMatch && req.method === "GET") {
|
|
997
|
+
const host = decodeURIComponent(serverDetailMatch[1]);
|
|
998
|
+
const detailKind = serverDetailMatch[2];
|
|
999
|
+
if (!host) return withCors(req, Response.json({ ok: false, error: "host required" }, { status: 400 }));
|
|
1000
|
+
|
|
1001
|
+
const cutoff = sqliteTime(new Date(Date.now() - 10 * 60 * 1000));
|
|
1002
|
+
const staleParams: any[] = [cutoff];
|
|
1003
|
+
let staleSql = "UPDATE sessions SET status = 'offline' WHERE updated_at < ?1 AND status != 'offline'";
|
|
1004
|
+
staleSql = addNetworkScope(staleSql, staleParams, restScope);
|
|
1005
|
+
db.run(staleSql, staleParams);
|
|
1006
|
+
|
|
1007
|
+
if (detailKind === "agents") {
|
|
1008
|
+
const params: any[] = [host, host];
|
|
1009
|
+
let sql = `
|
|
1010
|
+
SELECT alias, agent, status, task, progress, model, hostname, ip,
|
|
1011
|
+
cpu_load_1min, cpu_cores, mem_avail_gb, mem_used_gb, mem_total_gb,
|
|
1012
|
+
disk_avail_gb, disk_used_gb, disk_total_gb,
|
|
1013
|
+
process_rss_bytes, process_cpu_pct, process_uptime_seconds, process_in_flight_count,
|
|
1014
|
+
COALESCE(last_seen_at, updated_at) AS last_seen
|
|
1015
|
+
FROM sessions
|
|
1016
|
+
WHERE (hostname = ?1 OR ip = ?2)
|
|
1017
|
+
`;
|
|
1018
|
+
sql = addNetworkScope(sql, params, restScope);
|
|
1019
|
+
sql += " ORDER BY alias";
|
|
1020
|
+
const agents = db.all<any>(sql, ...params).map((s) => ({
|
|
1021
|
+
alias: s.alias,
|
|
1022
|
+
runtime: normalizeRuntime(s.agent),
|
|
1023
|
+
raw_agent: s.agent ?? null,
|
|
1024
|
+
model: s.model ?? null,
|
|
1025
|
+
status: s.status ?? "offline",
|
|
1026
|
+
task: s.task ?? null,
|
|
1027
|
+
progress: s.progress ?? 0,
|
|
1028
|
+
last_seen: s.last_seen ?? null,
|
|
1029
|
+
health: agentHealthChip(s.status, s.last_seen),
|
|
1030
|
+
hostname: s.hostname ?? null,
|
|
1031
|
+
ip: s.ip ?? null,
|
|
1032
|
+
telemetry: {
|
|
1033
|
+
cpu_load_1min: s.cpu_load_1min ?? null,
|
|
1034
|
+
cpu_cores: s.cpu_cores ?? null,
|
|
1035
|
+
cpu_pct: cpuPct(s.cpu_load_1min, s.cpu_cores),
|
|
1036
|
+
mem_total_gb: s.mem_total_gb ?? null,
|
|
1037
|
+
mem_used_gb: s.mem_used_gb ?? null,
|
|
1038
|
+
mem_avail_gb: s.mem_avail_gb ?? null,
|
|
1039
|
+
disk_total_gb: s.disk_total_gb ?? null,
|
|
1040
|
+
disk_used_gb: s.disk_used_gb ?? null,
|
|
1041
|
+
disk_avail_gb: s.disk_avail_gb ?? null,
|
|
1042
|
+
process_rss_bytes: s.process_rss_bytes ?? null,
|
|
1043
|
+
process_cpu_pct: s.process_cpu_pct ?? null,
|
|
1044
|
+
process_uptime_seconds: s.process_uptime_seconds ?? null,
|
|
1045
|
+
process_in_flight_count: s.process_in_flight_count ?? null,
|
|
1046
|
+
},
|
|
1047
|
+
}));
|
|
1048
|
+
if (agents.length === 0) return withCors(req, Response.json({ ok: false, error: "server not found" }, { status: 404 }));
|
|
1049
|
+
return withCors(req, Response.json({ ok: true, host, agent_count: agents.length, agents }));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const latestParams: any[] = [host, host];
|
|
1053
|
+
let latestSql = `
|
|
1054
|
+
SELECT hostname, ip, COUNT(*) OVER () AS agent_count,
|
|
1055
|
+
cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb,
|
|
1056
|
+
disk_total_gb, disk_used_gb, disk_avail_gb,
|
|
1057
|
+
COALESCE(last_seen_at, updated_at) AS last_seen
|
|
1058
|
+
FROM sessions
|
|
1059
|
+
WHERE (hostname = ?1 OR ip = ?2)
|
|
1060
|
+
`;
|
|
1061
|
+
latestSql = addNetworkScope(latestSql, latestParams, restScope);
|
|
1062
|
+
latestSql += " ORDER BY COALESCE(last_seen_at, updated_at) DESC LIMIT 1";
|
|
1063
|
+
const latest = db.get<any>(latestSql, ...latestParams);
|
|
1064
|
+
if (!latest) return withCors(req, Response.json({ ok: false, error: "server not found" }, { status: 404 }));
|
|
1065
|
+
|
|
1066
|
+
const since24h = sqliteTime(new Date(Date.now() - 24 * 60 * 60 * 1000));
|
|
1067
|
+
const histParams: any[] = [host, host, since24h];
|
|
1068
|
+
let histSql = `
|
|
1069
|
+
SELECT created_at, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb,
|
|
1070
|
+
disk_total_gb, disk_used_gb, disk_avail_gb
|
|
1071
|
+
FROM agent_telemetry
|
|
1072
|
+
WHERE (hostname = ?1 OR ip = ?2) AND created_at >= ?3
|
|
1073
|
+
`;
|
|
1074
|
+
histSql = addNetworkScope(histSql, histParams, restScope);
|
|
1075
|
+
histSql += " ORDER BY created_at ASC";
|
|
1076
|
+
const historyRows = db.all<any>(histSql, ...histParams);
|
|
1077
|
+
const now = Date.now();
|
|
1078
|
+
const alert = serverAlertLevel(latest);
|
|
1079
|
+
|
|
1080
|
+
return withCors(req, Response.json({
|
|
1081
|
+
ok: true,
|
|
1082
|
+
host,
|
|
1083
|
+
hostname: latest.hostname ?? null,
|
|
1084
|
+
ip: latest.ip ?? null,
|
|
1085
|
+
agent_count: latest.agent_count ?? 0,
|
|
1086
|
+
alert_level: alert.level,
|
|
1087
|
+
alerts: alert.alerts,
|
|
1088
|
+
latest: {
|
|
1089
|
+
cpu_load_1min: latest.cpu_load_1min ?? null,
|
|
1090
|
+
cpu_cores: latest.cpu_cores ?? null,
|
|
1091
|
+
cpu_pct: cpuPct(latest.cpu_load_1min, latest.cpu_cores),
|
|
1092
|
+
mem_total_gb: latest.mem_total_gb ?? null,
|
|
1093
|
+
mem_used_gb: latest.mem_used_gb ?? null,
|
|
1094
|
+
mem_avail_gb: latest.mem_avail_gb ?? null,
|
|
1095
|
+
disk_total_gb: latest.disk_total_gb ?? null,
|
|
1096
|
+
disk_used_gb: latest.disk_used_gb ?? null,
|
|
1097
|
+
disk_avail_gb: latest.disk_avail_gb ?? null,
|
|
1098
|
+
last_seen: latest.last_seen ?? null,
|
|
1099
|
+
},
|
|
1100
|
+
history: {
|
|
1101
|
+
"5m": bucketTelemetry(historyRows, now - 5 * 60 * 1000, 60 * 1000),
|
|
1102
|
+
"1h": bucketTelemetry(historyRows, now - 60 * 60 * 1000, 5 * 60 * 1000),
|
|
1103
|
+
"24h": bucketTelemetry(historyRows, now - 24 * 60 * 60 * 1000, 60 * 60 * 1000),
|
|
1104
|
+
},
|
|
1105
|
+
}));
|
|
1106
|
+
}
|
|
1107
|
+
|
|
897
1108
|
// ── REST: send task ──
|
|
898
1109
|
if (url.pathname === "/api/task" && req.method === "POST") {
|
|
899
1110
|
let raw: unknown;
|
package/src/tools.ts
CHANGED
|
@@ -127,9 +127,18 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
127
127
|
mem_total_gb: z.number().nullable().optional(),
|
|
128
128
|
mem_used_gb: z.number().nullable().optional(),
|
|
129
129
|
mem_avail_gb: z.number().nullable().optional(),
|
|
130
|
+
disk_total_gb: z.number().nullable().optional(),
|
|
131
|
+
disk_used_gb: z.number().nullable().optional(),
|
|
132
|
+
disk_avail_gb: z.number().nullable().optional(),
|
|
130
133
|
}).optional().describe("Host telemetry reported by agent-node"),
|
|
134
|
+
process_telemetry: z.object({
|
|
135
|
+
rss: z.number().nullable().optional(),
|
|
136
|
+
cpu_pct: z.number().nullable().optional(),
|
|
137
|
+
uptime_seconds: z.number().nullable().optional(),
|
|
138
|
+
in_flight_count: z.number().nullable().optional(),
|
|
139
|
+
}).optional().describe("Per-agent process telemetry reported by agent-node"),
|
|
131
140
|
},
|
|
132
|
-
async ({ resume_id, alias, status, task, output, score, progress, server: srv, hostname: hn, agent: ag, project_dir: pd, version: ver, tmux_name: tmux, node_id, session_id, config_path, channels, model: mdl, node_name: nn, network_id: netId, host }) => {
|
|
141
|
+
async ({ resume_id, alias, status, task, output, score, progress, server: srv, hostname: hn, agent: ag, project_dir: pd, version: ver, tmux_name: tmux, node_id, session_id, config_path, channels, model: mdl, node_name: nn, network_id: netId, host, process_telemetry: proc }) => {
|
|
133
142
|
const effectiveNetId = getNetworkId(netId);
|
|
134
143
|
const sessionNetId = effectiveNetId ?? "default";
|
|
135
144
|
if (!callerTokenIsNetwork || !enforceNetworkId) {
|
|
@@ -147,13 +156,20 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
147
156
|
const memTotalGb = typeof host?.mem_total_gb === "number" ? host.mem_total_gb : null;
|
|
148
157
|
const memUsedGb = typeof host?.mem_used_gb === "number" ? host.mem_used_gb : null;
|
|
149
158
|
const memAvailGb = typeof host?.mem_avail_gb === "number" ? host.mem_avail_gb : null;
|
|
159
|
+
const diskTotalGb = typeof host?.disk_total_gb === "number" ? host.disk_total_gb : null;
|
|
160
|
+
const diskUsedGb = typeof host?.disk_used_gb === "number" ? host.disk_used_gb : null;
|
|
161
|
+
const diskAvailGb = typeof host?.disk_avail_gb === "number" ? host.disk_avail_gb : null;
|
|
162
|
+
const processRssBytes = typeof proc?.rss === "number" ? proc.rss : null;
|
|
163
|
+
const processCpuPct = typeof proc?.cpu_pct === "number" ? proc.cpu_pct : null;
|
|
164
|
+
const processUptimeSeconds = typeof proc?.uptime_seconds === "number" ? proc.uptime_seconds : null;
|
|
165
|
+
const processInFlightCount = typeof proc?.in_flight_count === "number" ? proc.in_flight_count : null;
|
|
150
166
|
|
|
151
167
|
db.transaction(() => {
|
|
152
168
|
// Only delete same-alias sessions within the same network
|
|
153
169
|
db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2 AND network_id = ?3", [alias, resume_id, sessionNetId]);
|
|
154
170
|
db.run(
|
|
155
|
-
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, network_id, model, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, last_seen_at, updated_at)
|
|
156
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, datetime('now'), datetime('now'))
|
|
171
|
+
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, network_id, model, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb, process_rss_bytes, process_cpu_pct, process_uptime_seconds, process_in_flight_count, last_seen_at, updated_at)
|
|
172
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, datetime('now'), datetime('now'))
|
|
157
173
|
ON CONFLICT(resume_id) DO UPDATE SET
|
|
158
174
|
alias = COALESCE(?2, sessions.alias), tmux_name = COALESCE(?3, sessions.tmux_name),
|
|
159
175
|
server = COALESCE(?4, sessions.server), ip = COALESCE(?5, sessions.ip),
|
|
@@ -170,9 +186,23 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
170
186
|
mem_total_gb = COALESCE(?23, sessions.mem_total_gb),
|
|
171
187
|
mem_used_gb = COALESCE(?24, sessions.mem_used_gb),
|
|
172
188
|
mem_avail_gb = COALESCE(?25, sessions.mem_avail_gb),
|
|
189
|
+
disk_total_gb = COALESCE(?26, sessions.disk_total_gb),
|
|
190
|
+
disk_used_gb = COALESCE(?27, sessions.disk_used_gb),
|
|
191
|
+
disk_avail_gb = COALESCE(?28, sessions.disk_avail_gb),
|
|
192
|
+
process_rss_bytes = COALESCE(?29, sessions.process_rss_bytes),
|
|
193
|
+
process_cpu_pct = COALESCE(?30, sessions.process_cpu_pct),
|
|
194
|
+
process_uptime_seconds = COALESCE(?31, sessions.process_uptime_seconds),
|
|
195
|
+
process_in_flight_count = COALESCE(?32, sessions.process_in_flight_count),
|
|
173
196
|
last_seen_at = datetime('now'), updated_at = datetime('now')`,
|
|
174
|
-
[resume_id, alias, tmux ?? null, srv ?? null, hostIp, hostHostname, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null, sessionNetId, mdl ?? null, cpuLoad1m, cpuCores, memTotalGb, memUsedGb, memAvailGb]
|
|
197
|
+
[resume_id, alias, tmux ?? null, srv ?? null, hostIp, hostHostname, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null, sessionNetId, mdl ?? null, cpuLoad1m, cpuCores, memTotalGb, memUsedGb, memAvailGb, diskTotalGb, diskUsedGb, diskAvailGb, processRssBytes, processCpuPct, processUptimeSeconds, processInFlightCount]
|
|
175
198
|
);
|
|
199
|
+
if (host || proc) {
|
|
200
|
+
db.run(
|
|
201
|
+
`INSERT INTO agent_telemetry (id, network_id, resume_id, alias, hostname, ip, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb, process_rss_bytes, process_cpu_pct, process_uptime_seconds, process_in_flight_count, created_at)
|
|
202
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, datetime('now'))`,
|
|
203
|
+
[uuidv4(), sessionNetId, resume_id, alias, hostHostname, hostIp, cpuLoad1m, cpuCores, memTotalGb, memUsedGb, memAvailGb, diskTotalGb, diskUsedGb, diskAvailGb, processRssBytes, processCpuPct, processUptimeSeconds, processInFlightCount]
|
|
204
|
+
);
|
|
205
|
+
}
|
|
176
206
|
});
|
|
177
207
|
|
|
178
208
|
// V2: sync tasks table — report_status(working) → tasks.running
|