@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 +1 -1
- package/src/db.ts +1 -1
- package/src/human-inbox.ts +42 -0
- package/src/tools.ts +14 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.9.0-preview.
|
|
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
|
|
1431
|
+
"Read inbox.",
|
|
1431
1432
|
{
|
|
1432
|
-
alias: z.string().min(1).max(200)
|
|
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
|
-
|
|
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
|
);
|