@sleep2agi/commhub-server 0.8.2 → 0.8.3-preview.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +46 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.8.2",
3
+ "version": "0.8.3-preview.1",
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/index.ts CHANGED
@@ -342,6 +342,7 @@ const TaskSchema = z.object({
342
342
  priority: z.enum(["high", "normal", "low"]).default("normal"),
343
343
  from: z.string().max(200).optional(),
344
344
  network_id: z.string().max(200).optional(),
345
+ parent_task_id: z.string().max(200).optional(),
345
346
  });
346
347
 
347
348
  const BroadcastSchema = z.object({
@@ -987,13 +988,38 @@ Bun.serve({
987
988
  last_seen: string | null;
988
989
  }>();
989
990
 
991
+ const preferDisplayIp = (current: string, next: string) => {
992
+ const isWeak = (ip: string) => !ip || ip === "unknown" || ip === "127.0.0.1" || ip === "::1";
993
+ if (isWeak(current) && !isWeak(next)) return next;
994
+ return current;
995
+ };
996
+ const hasHostTelemetry = (row: any) =>
997
+ row.cpu_load_1min != null || row.cpu_cores != null || row.mem_avail_gb != null || row.mem_used_gb != null;
998
+
990
999
  for (const row of db.all<any>(sql, ...params)) {
991
1000
  const hostname = row.hostname || "unknown";
992
1001
  const ip = row.ip || "unknown";
993
- const key = `${hostname}\u0000${ip}`;
1002
+ // Group primarily by hostname. A single host can report both a
1003
+ // routable/container IP and loopback (127.0.0.1); splitting those
1004
+ // into separate cards makes the dashboard show one useful load row
1005
+ // plus one "n/a" duplicate. Unknown hostnames still fall back to IP.
1006
+ const key = hostname !== "unknown" ? `host:${hostname}` : `ip:${ip}`;
994
1007
  const existing = grouped.get(key);
995
1008
  if (existing) {
996
1009
  existing.agent_count += 1;
1010
+ existing.ip = preferDisplayIp(existing.ip, ip);
1011
+ if (parseSqliteTime(row.last_seen) > parseSqliteTime(existing.last_seen)) existing.last_seen = row.last_seen ?? existing.last_seen;
1012
+ if (hasHostTelemetry(row) && (
1013
+ existing.cpu_load_1min == null ||
1014
+ existing.cpu_cores == null ||
1015
+ existing.mem_avail_gb == null ||
1016
+ existing.mem_used_gb == null
1017
+ )) {
1018
+ existing.cpu_load_1min = row.cpu_load_1min ?? existing.cpu_load_1min;
1019
+ existing.cpu_cores = row.cpu_cores ?? existing.cpu_cores;
1020
+ existing.mem_avail_gb = row.mem_avail_gb ?? existing.mem_avail_gb;
1021
+ existing.mem_used_gb = row.mem_used_gb ?? existing.mem_used_gb;
1022
+ }
997
1023
  continue;
998
1024
  }
999
1025
  grouped.set(key, {
@@ -1177,9 +1203,9 @@ Bun.serve({
1177
1203
  [id, body.alias, body.priority, body.task, fromSession, taskNetId]
1178
1204
  );
1179
1205
  db.run(
1180
- `INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id)
1181
- VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7)`,
1182
- [id, fromSession, body.alias, body.priority, body.task, `+${ttlSeconds} seconds`, taskNetId]
1206
+ `INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id, parent_task_id)
1207
+ VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7, ?8)`,
1208
+ [id, fromSession, body.alias, body.priority, body.task, `+${ttlSeconds} seconds`, taskNetId, body.parent_task_id ?? null]
1183
1209
  );
1184
1210
  // Touch session row so the dashboard reflects "task in flight"
1185
1211
  // immediately, without waiting for the agent's report_status to
@@ -1200,7 +1226,7 @@ Bun.serve({
1200
1226
  if (taskNetId) { sessionSql += " AND network_id = ?2"; sessionParams.push(taskNetId); }
1201
1227
  const targetSession = db.get<any>(sessionSql, ...sessionParams);
1202
1228
  if (targetSession) pushEvent(body.alias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority: body.priority, from: fromSession }, taskNetId);
1203
- return withCors(req, Response.json({ ok: true, message_id: id }));
1229
+ return withCors(req, Response.json({ ok: true, task_id: id, message_id: id }));
1204
1230
  }
1205
1231
 
1206
1232
  // ── REST: broadcast ──
@@ -1460,6 +1486,21 @@ Bun.serve({
1460
1486
  return withCors(req, Response.json({ ok: true, nodes: rows, count: rows.length }));
1461
1487
  }
1462
1488
 
1489
+ // ── REST: single task lookup (V2) ──
1490
+ const taskPathMatch = url.pathname.match(/^\/api\/tasks?\/([^/]+)$/);
1491
+ if (taskPathMatch && req.method === "GET") {
1492
+ const taskId = decodeURIComponent(taskPathMatch[1] ?? "");
1493
+ const params: any[] = [taskId];
1494
+ let sql = "SELECT * FROM tasks WHERE task_id = ?1";
1495
+ sql = addNetworkScope(sql, params, restScope);
1496
+ sql += " LIMIT 1";
1497
+ const task = db.get(sql, ...params);
1498
+ if (!task) {
1499
+ return withCors(req, Response.json({ ok: false, error: "task_not_found", task_id: taskId }, { status: 404 }));
1500
+ }
1501
+ return withCors(req, Response.json({ ok: true, task }));
1502
+ }
1503
+
1463
1504
  // ── REST: tasks table (V2) ──
1464
1505
  if (url.pathname === "/api/tasks") {
1465
1506
  const taskId = url.searchParams.get("task_id");