@sleep2agi/commhub-server 0.9.0-preview.138 → 0.9.0-preview.139

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.138",
3
+ "version": "0.9.0-preview.139",
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",
package/src/db.ts CHANGED
@@ -2228,7 +2228,7 @@ export function logTaskEvent(taskId: string, fromStatus: string | null, toStatus
2228
2228
  // RFC-039 F3: queries break created_at ties with SQLite's implicit rowid
2229
2229
  // (insertion order). PostgreSQL has none, so give these tables a real one.
2230
2230
  if (db.dialect === "postgres") {
2231
- for (const table of ["nodes", "user_inbox"]) db.exec(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS rowid BIGSERIAL`);
2231
+ for (const table of ["nodes", "user_inbox", "inbox"]) db.exec(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS rowid BIGSERIAL`);
2232
2232
  }
2233
2233
 
2234
2234
  // Board #625: common SQLite/PostgreSQL DDL. Adoption records authority, not processes.
@@ -0,0 +1,42 @@
1
+ import { db } from "./db.js";
2
+
3
+ /** Metadata only: the body of an ordinary blocked task is never fetched by this scan. */
4
+ export function isAuthenticatedHumanInboxRow(row: { type: string; meta_json: unknown }): boolean {
5
+ if (row.type !== "task" && row.type !== "broadcast") return false;
6
+ let parsed: unknown;
7
+ try { parsed = typeof row.meta_json === "string" ? JSON.parse(row.meta_json) : null; }
8
+ catch { return false; }
9
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
10
+ const meta = parsed as Record<string, unknown>;
11
+ return meta.auth_origin === "user" && meta.source === "dashboard-chat"
12
+ && typeof meta.client_request_id === "string" && /^dreq_[a-f0-9]{32}$/.test(meta.client_request_id);
13
+ }
14
+
15
+ /** Caller supplies the already-authorized recipient/network WHERE clause. Both DB dialects
16
+ * support this scalar keyset; no JSON SQL extension, OFFSET, or arbitrary depth cutoff. */
17
+ export function selectHumanInboxIds(whereSql: string, params: unknown[], limit: number) {
18
+ const ids: string[] = [];
19
+ let cursor: { priority_rank: number; created_at: string; fifo_id: number } | undefined;
20
+ let examined = 0;
21
+ const rank = "CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 ELSE 2 END";
22
+ while (ids.length < limit) {
23
+ const pageParams = [...params];
24
+ let sql = `SELECT id, type, meta_json, created_at, rowid AS fifo_id, ${rank} AS priority_rank ${whereSql}`;
25
+ if (cursor) {
26
+ const n = pageParams.length + 1;
27
+ sql += ` AND (${rank}, created_at, rowid) > (?${n}, ?${n + 1}, ?${n + 2})`;
28
+ pageParams.push(cursor.priority_rank, cursor.created_at, cursor.fifo_id);
29
+ }
30
+ sql += ` ORDER BY ${rank}, created_at, rowid LIMIT 256`;
31
+ const page = db.all<{ id: string; type: string; meta_json: unknown; priority_rank: number; created_at: string; fifo_id: number }>(sql, ...pageParams);
32
+ if (!page.length) break;
33
+ for (const row of page) {
34
+ examined++;
35
+ if (isAuthenticatedHumanInboxRow(row)) ids.push(row.id);
36
+ if (ids.length === limit) break;
37
+ }
38
+ cursor = page[page.length - 1];
39
+ if (page.length < 256) break;
40
+ }
41
+ return { ids, examined };
42
+ }
package/src/tools.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { selectHumanInboxIds } from "./human-inbox.js";
1
2
  import { hostSupervisorIdentityRows } from "./host-supervisor-identity.js";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { markTaskActivity } from "./task-activity.js";
@@ -1427,13 +1428,14 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1427
1428
 
1428
1429
  server.tool(
1429
1430
  "get_inbox",
1430
- "Read your inbox.",
1431
+ "Read inbox.",
1431
1432
  {
1432
- alias: z.string().min(1).max(200).describe("Alias"),
1433
+ alias: z.string().min(1).max(200),
1433
1434
  limit: z.number().min(1).max(100).optional().default(10),
1434
1435
  network_id: z.string().max(200).optional(),
1436
+ human_first: z.boolean().optional(),
1435
1437
  },
1436
- async ({ alias, limit, network_id: netId }) => {
1438
+ async ({ alias, limit, network_id: netId, human_first }) => {
1437
1439
  const effectiveNetId = getNetworkId(netId);
1438
1440
  const recipient = resolveInboxCaller(alias, { tokenId: callerTokenId, userId: enforceUserId, networkId: effectiveNetId, nodeToken: callerTokenIsNetwork, identityRejected: callerIdentityRejected });
1439
1441
  if (!recipient.ok) return { content: [{ type: "text" as const, text: JSON.stringify(recipient) }] };
@@ -1445,13 +1447,20 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1445
1447
  countSql = addReadScope(countSql, countParams, readScope);
1446
1448
  const rows0 = db.get<{ cnt: number }>(countSql, ...countParams);
1447
1449
  console.log(`[${ts()}] ${alias} → get_inbox: ${rows0?.cnt ?? 0} pending messages`);
1450
+ const scanParams: any[] = [alias];
1451
+ const scanWhere = addReadScope("FROM inbox WHERE session_name = ?1 AND acked = 0", scanParams, readScope);
1452
+ const human = human_first ? selectHumanInboxIds(scanWhere, scanParams, limit) : undefined;
1448
1453
  const rowsParams: any[] = [alias];
1449
1454
  let rowsSql = `SELECT id, type, priority, content, context, from_session, created_at, network_id, meta_json,
1450
1455
  CASE WHEN EXISTS(SELECT 1 FROM task_late_replies lr WHERE lr.inbox_id=inbox.id) THEN 1 ELSE 0 END AS late,
1451
1456
  CASE WHEN type = 'task' THEN COALESCE(task_id, id) ELSE task_id END AS task_id
1452
1457
  FROM inbox WHERE session_name = ?1 AND acked = 0`;
1453
1458
  rowsSql = addReadScope(rowsSql, rowsParams, readScope);
1454
- rowsSql += ` ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 ELSE 2 END, created_at
1459
+ if (human?.ids.length) {
1460
+ const placeholders = human.ids.map((id) => { rowsParams.push(id); return `?${rowsParams.length}`; });
1461
+ rowsSql += ` ORDER BY CASE WHEN id IN (${placeholders.join(",")}) THEN 0 ELSE 1 END,`;
1462
+ } else rowsSql += " ORDER BY";
1463
+ rowsSql += ` CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 ELSE 2 END, created_at, rowid
1455
1464
  LIMIT ?${rowsParams.length + 1}`;
1456
1465
  rowsParams.push(limit);
1457
1466
  const rows = db.all(rowsSql, ...rowsParams).map((row: any) => {
@@ -1461,7 +1470,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1461
1470
  });
1462
1471
 
1463
1472
  return {
1464
- content: [{ type: "text" as const, text: JSON.stringify({ ok: true, messages: rows }) }],
1473
+ content: [{ type: "text" as const, text: JSON.stringify({ ok: true, messages: rows, ...(human ? { human_inbox: { version: 1, mode: "metadata-keyset", examined: human.examined } } : {}) }) }],
1465
1474
  };
1466
1475
  }
1467
1476
  );