@sleep2agi/commhub-server 0.9.0-preview.37 → 0.9.0-preview.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.9.0-preview.37",
3
+ "version": "0.9.0-preview.38",
4
4
  "description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -306,6 +306,7 @@ export function auditCreateNodeStrict(input: {
306
306
  export function auditCreateNode(input: {
307
307
  action:
308
308
  | "create_node_dispatched"
309
+ | "create_node_dispatch_attempted"
309
310
  | "create_node_rejected"
310
311
  | "create_node_succeeded"
311
312
  | "create_node_sweeper_revoked"
@@ -36,6 +36,7 @@ interface Reply { ok?: boolean; error?: string; [k: string]: unknown; }
36
36
  function cleanup() {
37
37
  for (const n of [NET_A, NET_B]) {
38
38
  try { db.run("DELETE FROM node_stop_requests WHERE network_id = ?1", [n]); } catch {}
39
+ try { db.run("DELETE FROM node_create_requests WHERE network_id = ?1", [n]); } catch {}
39
40
  try { db.run("DELETE FROM audit_log WHERE network_id = ?1", [n]); } catch {}
40
41
  try { db.run("DELETE FROM inbox WHERE network_id = ?1", [n]); } catch {}
41
42
  // PR1.2a restart_node tests leave node_config_updates rows; clear
@@ -676,6 +677,60 @@ describe("list_my_children HANDLER (RFC-027 PR1.1)", () => {
676
677
  });
677
678
  });
678
679
 
680
+ describe("create_node audit wording (#1362)", () => {
681
+ test("create_node records a dispatch attempt, not guaranteed SSE delivery", async () => {
682
+ setupAlphaNetwork();
683
+ const tools = buildHandlers(USER_A_ID);
684
+ const r = await call(tools.create_node, {
685
+ daemon_node_id: DAEMON_A_ID,
686
+ node_spec: {
687
+ name: "sd-new-child",
688
+ runtime: "claude-agent-sdk",
689
+ model: "x",
690
+ },
691
+ network_id: NET_A,
692
+ });
693
+
694
+ expect(r.ok).toBe(true);
695
+ expect(typeof r.request_id).toBe("string");
696
+ expect(readAudit("create_node_dispatch_attempted", NET_A).length).toBe(1);
697
+ expect(readAudit("create_node_dispatched", NET_A).length).toBe(0);
698
+ });
699
+ });
700
+
701
+ describe("list_my_pending_create_requests HANDLER (#1362 SSE reconnect compensation)", () => {
702
+ test("daemon-bound caller gets only its own pending create requests", async () => {
703
+ setupAlphaNetwork();
704
+ const otherDaemonId = "node_sd_daemon_other";
705
+ seedDaemon(NET_A, otherDaemonId, "sd-daemon-other", USER_A_ID, "tok_sd_daemon_other");
706
+ db.run(
707
+ `INSERT INTO node_create_requests
708
+ (request_id, daemon_node_id, child_name, network_id, runtime, model, flags_json, env_keys, status, child_token_id, created_at, created_by_token)
709
+ VALUES
710
+ ('cr_pending_mine', ?1, 'child-pending-mine', ?2, 'claude-agent-sdk', 'x', '{}', '[]', 'pending', NULL, ?3, 'tok_test'),
711
+ ('cr_delivered_mine', ?1, 'child-delivered-mine', ?2, 'claude-agent-sdk', 'x', '{}', '[]', 'delivered', NULL, ?4, 'tok_test'),
712
+ ('cr_pending_other', ?5, 'child-pending-other', ?2, 'claude-agent-sdk', 'x', '{}', '[]', 'pending', NULL, ?6, 'tok_test')`,
713
+ [DAEMON_A_ID, NET_A, Date.now(), Date.now() + 1, otherDaemonId, Date.now() + 2],
714
+ );
715
+
716
+ const daemonTools = buildHandlers(USER_A_ID, true, DAEMON_A_TOK, NET_A);
717
+ const r = await call(daemonTools.list_my_pending_create_requests, {});
718
+
719
+ expect(r.ok).toBe(true);
720
+ expect(r.count).toBe(1);
721
+ expect(r.requests).toEqual([
722
+ expect.objectContaining({ request_id: "cr_pending_mine", child_name: "child-pending-mine" }),
723
+ ]);
724
+ });
725
+
726
+ test("non-daemon caller (utok) refused", async () => {
727
+ setupAlphaNetwork();
728
+ const userTools = buildHandlers(USER_A_ID);
729
+ const r = await call(userTools.list_my_pending_create_requests, {});
730
+ expect(r.ok).toBe(false);
731
+ });
732
+ });
733
+
679
734
  // ─── PR1.2a — restart_node resets lifecycle_state ──────────────────
680
735
  //
681
736
  // 通信龙 #346 ack latent: without this, a stopped node restart-ed via
package/src/tools.ts CHANGED
@@ -3215,9 +3215,10 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
3215
3215
  // SSE handler resolves the rest via MCP call.
3216
3216
  pushEvent(daemon.alias, { type: "create_node", request_id: requestId }, networkIdForChild);
3217
3217
 
3218
- // §4.5 audit — dispatch succeeded
3218
+ // §4.5 audit — request queued and doorbell attempted. pushEvent is
3219
+ // fire-and-forget, so actual SSE delivery is intentionally not claimed.
3219
3220
  auditCreateNode({
3220
- action: "create_node_dispatched",
3221
+ action: "create_node_dispatch_attempted",
3221
3222
  user_id: enforceUserId,
3222
3223
  network_id: networkIdForChild,
3223
3224
  target_id: requestId,
@@ -3237,6 +3238,31 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
3237
3238
  },
3238
3239
  );
3239
3240
 
3241
+ // #1362 — daemon-side compensation for missed SSE doorbells. pushEvent is
3242
+ // intentionally best-effort; after a daemon connects or reconnects, it pulls
3243
+ // any still-pending requests bound to its own daemon token.
3244
+ server.tool(
3245
+ "list_my_pending_create_requests",
3246
+ "Daemon lists pending create-node requests bound to itself for SSE reconnect compensation. RFC-026/#1362.",
3247
+ {},
3248
+ async () => {
3249
+ const callerDaemon = resolveCallerDaemonTokenBound();
3250
+ if (!callerDaemon.ok) {
3251
+ return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: callerDaemon.error }) }] };
3252
+ }
3253
+ const rows = db.all<{ request_id: string; child_name: string; created_at: number }>(
3254
+ `SELECT request_id, child_name, created_at
3255
+ FROM node_create_requests
3256
+ WHERE daemon_node_id = ?1
3257
+ AND network_id = ?2
3258
+ AND status = 'pending'
3259
+ ORDER BY created_at ASC`,
3260
+ callerDaemon.daemonNodeId, callerDaemon.networkId,
3261
+ );
3262
+ return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, count: rows.length, requests: rows }) }] };
3263
+ },
3264
+ );
3265
+
3240
3266
  // §2.5 step 3 — daemon-facing pull. Returns full spec + env_blob +
3241
3267
  // child_ntok in one shot. Map evicted on take (one-shot consume).
3242
3268
  //