@sleep2agi/commhub-server 0.9.0-preview.21 → 0.9.0-preview.23

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/src/tools.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod/v4";
3
3
  import { db, uuidv4, logTaskEvent, chainReplyToParent, hashToken, generateId, generateNetworkToken } from "./db.js";
4
- import { pushEvent } from "./push.js";
4
+ import { pushEvent, pushNetworkObserverEvent } from "./push.js";
5
5
  import { assertNodeActive } from "./lifecycle-guard.js";
6
6
  import { getUserNetworkRole, createNetworkTokenForNode } from "./auth.js";
7
7
  import {
@@ -958,6 +958,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
958
958
  if (target.state === "online") {
959
959
  pushEvent(targetAlias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority, from: from_session, ...(canonical.renamed ? { renamed_from: alias } : {}) }, effectiveNetId);
960
960
  }
961
+ // #461 network observer summary — unconditional (task row exists
962
+ // even when the target is offline/queued), metadata only.
963
+ pushNetworkObserverEvent(effectiveNetId, { type: "new_task", task_id: id, from: from_session, to: targetAlias, status: target.state === "online" ? "delivered" : "queued", priority });
961
964
 
962
965
  if (target.state === "offline") {
963
966
  return {
@@ -1055,7 +1058,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1055
1058
  // ── V2: send_reply (关联 task_id,不触发 think) ──
1056
1059
  server.tool(
1057
1060
  "send_reply",
1058
- "Send a reply to a task. Linked to task_id via in_reply_to. Does NOT trigger agent processing.",
1061
+ "Send a reply to a Dashboard/UI-originated task (updates task row, emits new_reply SSE — Dashboard reads it). ⚠ NOT for agent-to-agent replies: agent nodes only log new_reply and do not processInbox on it, so the peer will not see the reply in real time. Use send_task for peer-to-peer replies (RFC-030, Vincent 2026-07-28 全网规则). If the target alias resolves to a live agent node, the response includes a `warning` field pointing to send_task.",
1059
1062
  {
1060
1063
  alias: z.string().min(1).max(200).describe("Target session alias"),
1061
1064
  text: z.string().min(1).max(10000).describe("Reply content"),
@@ -1185,11 +1188,29 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1185
1188
 
1186
1189
  const session = scopedSessionStatus(alias, effectiveNetId);
1187
1190
  pushEvent(alias, { type: "new_reply", from: from_session, message_id: id, in_reply_to, status: replyStatus }, effectiveNetId);
1191
+ // #461 network observer summary — ids + routing only, no reply text.
1192
+ pushNetworkObserverEvent(effectiveNetId, { type: "new_reply", task_id: in_reply_to ?? null, message_id: id, from: from_session, to: alias, status: replyStatus });
1193
+
1194
+ // #498 — When the reply target is a live agent node (not Dashboard/UI),
1195
+ // warn the caller that agent peers only log new_reply and do not
1196
+ // processInbox on it. Points at the correct next action (send_task)
1197
+ // so a follow-up turn corrects course rather than "why didn't they
1198
+ // respond" spelunking. Absent field when target is Dashboard/hub/api
1199
+ // (the correct path for commhub_reply) — silence is affirmation.
1200
+ const targetIsAgent = replyTargetNodeId !== null && alias !== "hub" && alias !== "api";
1201
+ const warning = targetIsAgent
1202
+ ? `Target "${alias}" is an agent node. commhub_reply/send_reply does NOT wake agent peers (they only log new_reply, they do not processInbox on it) — the peer will not see this reply in real time. For agent-to-agent replies, use commhub_send_task(alias="${alias}", task="<your reply>") so the peer wakes via new_task SSE and processes it. (RFC-030, Vincent 2026-07-28 全网规则.)`
1203
+ : undefined;
1188
1204
 
1189
1205
  return {
1190
1206
  content: [{
1191
1207
  type: "text" as const,
1192
- text: JSON.stringify({ ok: true, message_id: id, session_status: session?.status ?? "unknown" }),
1208
+ text: JSON.stringify({
1209
+ ok: true,
1210
+ message_id: id,
1211
+ session_status: session?.status ?? "unknown",
1212
+ ...(warning ? { warning } : {}),
1213
+ }),
1193
1214
  }],
1194
1215
  };
1195
1216
  }
@@ -1267,6 +1288,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1267
1288
  logTaskEvent(task_id, task.status, "delivered", from_session, "retry");
1268
1289
  // SSE push (unconditional — channel is keyed by alias, not network)
1269
1290
  pushEvent(task.to_name, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session }, effectiveNetId ?? task.network_id ?? null);
1291
+ pushNetworkObserverEvent(effectiveNetId ?? task.network_id ?? null, { type: "new_task", task_id, from: from_session, to: task.to_name, status: "delivered", priority: task.priority });
1270
1292
  return {
1271
1293
  content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, retried_to: task.to_name }) }],
1272
1294
  };
@@ -1419,6 +1441,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1419
1441
  });
1420
1442
  logTaskEvent(task_id, task.status, "delivered", from_session, `reassign: ${oldAlias} → ${reassignedAlias}`);
1421
1443
  pushEvent(reassignedAlias, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session, ...(canonical.renamed ? { renamed_from: new_alias } : {}) }, effectiveNetId ?? task.network_id ?? null);
1444
+ pushNetworkObserverEvent(effectiveNetId ?? task.network_id ?? null, { type: "new_task", task_id, from: from_session, to: reassignedAlias, status: "delivered", priority: task.priority });
1422
1445
  return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, reassigned_from: oldAlias, reassigned_to: reassignedAlias, ...(canonical.renamed ? { renamed_from: new_alias, renamed_to: reassignedAlias } : {}) }) }] };
1423
1446
  }
1424
1447
  );
@@ -14,8 +14,11 @@ import { register, login } from "./auth.js";
14
14
 
15
15
  const SERVER_DB = mkdtempSync(join(tmpdir(), "anet-upload-http-db-")) + "/commhub.db";
16
16
  const UPLOADS_DIR = mkdtempSync(join(tmpdir(), "anet-upload-http-fs-"));
17
- const PORT = 19000 + Math.floor(Math.random() * 1000);
18
- const BASE = `http://127.0.0.1:${PORT}`;
17
+ // #438 corrective: private bootServer({ port: 0 }) instance — the OS
18
+ // assigns the port and BASE is derived from the ACTUAL bound port, so
19
+ // this suite is immune to both the parent-side random-port TOCTOU and
20
+ // the aggregate module-cache no-op (import no longer boots anything).
21
+ let BASE = "";
19
22
 
20
23
  let server: any;
21
24
  let userToken = "";
@@ -27,7 +30,6 @@ beforeAll(async () => {
27
30
  // we want to validate the production code path).
28
31
  process.env.COMMHUB_DB = SERVER_DB;
29
32
  process.env.COMMHUB_UPLOADS_DIR = UPLOADS_DIR;
30
- process.env.PORT = String(PORT);
31
33
  process.env.HOST = "127.0.0.1";
32
34
 
33
35
  // Use a unique username + strong password each run so we don't trip
@@ -50,16 +52,17 @@ beforeAll(async () => {
50
52
  }
51
53
  expect(userToken).toBeTruthy();
52
54
 
53
- // Import the server module — its top-level `Bun.serve` call binds
54
- // the port immediately. The import side effect IS the server start.
55
- await import("./index.js");
55
+ // Importing is side-effect-free now; boot a private instance and
56
+ // read the real port back.
57
+ const { bootServer } = await import("./server.js");
58
+ server = bootServer({ port: 0, hostname: "127.0.0.1" });
59
+ BASE = `http://127.0.0.1:${server.port}`;
56
60
  // Tiny settle so the listener is ready.
57
61
  await new Promise((r) => setTimeout(r, 100));
58
62
  });
59
63
 
60
64
  afterAll(() => {
61
- // Best-effort cleanup. The Bun.serve handle is created at import
62
- // time so we can't easily stop it — process exit handles that.
65
+ try { server?.stop?.(true); } catch {}
63
66
  try { rmSync(UPLOADS_DIR, { recursive: true, force: true }); } catch {}
64
67
  try { rmSync(SERVER_DB, { recursive: true, force: true }); } catch {}
65
68
  });