@sleep2agi/commhub-server 0.5.0-preview.2 → 0.5.0-preview.21
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/README.md +110 -0
- package/package.json +1 -1
- package/src/auth.ts +164 -0
- package/src/db-adapter.ts +72 -0
- package/src/db.ts +155 -0
- package/src/index.ts +351 -12
- package/src/tools.ts +242 -22
package/src/tools.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod/v4";
|
|
3
|
-
import { db, uuidv4 } from "./db.js";
|
|
3
|
+
import { db, uuidv4, logTaskEvent } from "./db.js";
|
|
4
4
|
import { pushEvent, pushBroadcast } from "./push.js";
|
|
5
5
|
|
|
6
6
|
function ts(): string {
|
|
7
7
|
return new Date().toTimeString().slice(0, 8);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function registerTools(server: McpServer, clientIP?: string) {
|
|
10
|
+
export function registerTools(server: McpServer, clientIP?: string, enforceNetworkId?: string | null) {
|
|
11
|
+
// If enforceNetworkId is set, override any client-supplied network_id
|
|
12
|
+
const getNetworkId = (clientNetId?: string | null) => enforceNetworkId ?? clientNetId ?? null;
|
|
11
13
|
// ═══════════════════════════════════════════
|
|
12
14
|
// Child Agent Tools (4)
|
|
13
15
|
// ═══════════════════════════════════════════
|
|
@@ -35,17 +37,25 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
35
37
|
config_path: z.string().max(1000).optional().describe("Config file path"),
|
|
36
38
|
channels: z.string().max(2000).optional().describe("JSON array of channels"),
|
|
37
39
|
model: z.string().max(200).optional().describe("AI model name"),
|
|
40
|
+
node_name: z.string().max(200).optional().describe("Stable node display name (may differ from alias)"),
|
|
41
|
+
network_id: z.string().max(200).optional().describe("Network this agent belongs to"),
|
|
38
42
|
},
|
|
39
|
-
async ({ resume_id, alias, status, task, output, score, progress, server: srv, hostname: hn, agent: ag, project_dir: pd, version: ver, tmux_name: tmux, node_id, session_id, config_path, channels, model: mdl }) => {
|
|
40
|
-
|
|
43
|
+
async ({ resume_id, alias, status, task, output, score, progress, server: srv, hostname: hn, agent: ag, project_dir: pd, version: ver, tmux_name: tmux, node_id, session_id, config_path, channels, model: mdl, node_name: nn, network_id: netId }) => {
|
|
44
|
+
const effectiveNetId = getNetworkId(netId);
|
|
45
|
+
console.log(`[${ts()}] ${alias} (${resume_id.slice(0, 8)}) → report_status: ${status}${task ? " | " + task.slice(0, 60) : ""}${effectiveNetId ? " [net]" : ""}`);
|
|
41
46
|
const trimmedOutput = output?.slice(0, 4000);
|
|
42
47
|
|
|
43
48
|
try {
|
|
44
49
|
db.run("BEGIN IMMEDIATE");
|
|
45
|
-
|
|
50
|
+
// Only delete same-alias sessions within the same network (prevent cross-network alias conflict)
|
|
51
|
+
if (effectiveNetId) {
|
|
52
|
+
db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2 AND network_id = ?3", [alias, resume_id, effectiveNetId]);
|
|
53
|
+
} else {
|
|
54
|
+
db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2", [alias, resume_id]);
|
|
55
|
+
}
|
|
46
56
|
db.run(
|
|
47
|
-
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, last_seen_at, updated_at)
|
|
48
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, datetime('now'), datetime('now'))
|
|
57
|
+
`INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, network_id, last_seen_at, updated_at)
|
|
58
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, datetime('now'), datetime('now'))
|
|
49
59
|
ON CONFLICT(resume_id) DO UPDATE SET
|
|
50
60
|
alias = COALESCE(?2, sessions.alias),
|
|
51
61
|
tmux_name = COALESCE(?3, sessions.tmux_name),
|
|
@@ -64,9 +74,10 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
64
74
|
session_id = COALESCE(?16, sessions.session_id),
|
|
65
75
|
config_path = COALESCE(?17, sessions.config_path),
|
|
66
76
|
channels = COALESCE(?18, sessions.channels),
|
|
77
|
+
network_id = COALESCE(?19, sessions.network_id),
|
|
67
78
|
last_seen_at = datetime('now'),
|
|
68
79
|
updated_at = datetime('now')`,
|
|
69
|
-
[resume_id, alias, tmux ?? null, srv ?? null, clientIP ?? null, hn ?? null, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null]
|
|
80
|
+
[resume_id, alias, tmux ?? null, srv ?? null, clientIP ?? null, hn ?? null, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null, netId ?? null]
|
|
70
81
|
);
|
|
71
82
|
db.run("COMMIT");
|
|
72
83
|
} catch (e) {
|
|
@@ -77,11 +88,18 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
77
88
|
// V2: sync tasks table — report_status(working) → tasks.running
|
|
78
89
|
if (status === "working" && task) {
|
|
79
90
|
try {
|
|
80
|
-
db.run(
|
|
91
|
+
const runResult = db.run(
|
|
81
92
|
`UPDATE tasks SET status = 'running', started_at = datetime('now')
|
|
82
93
|
WHERE to_name = ?1 AND status IN ('delivered', 'acked') AND content = ?2`,
|
|
83
94
|
[alias, task]
|
|
84
95
|
);
|
|
96
|
+
if (runResult.changes > 0) {
|
|
97
|
+
// Find task_id for logging
|
|
98
|
+
const t = db.query<{ task_id: string }, [string, string]>(
|
|
99
|
+
"SELECT task_id FROM tasks WHERE to_name = ?1 AND content = ?2 AND status = 'running' ORDER BY started_at DESC LIMIT 1"
|
|
100
|
+
).get(alias, task);
|
|
101
|
+
if (t) logTaskEvent(t.task_id, null, "running", alias);
|
|
102
|
+
}
|
|
85
103
|
} catch {}
|
|
86
104
|
}
|
|
87
105
|
|
|
@@ -103,7 +121,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
103
121
|
server = COALESCE(?8, nodes.server),
|
|
104
122
|
hostname = COALESCE(?9, nodes.hostname),
|
|
105
123
|
updated_at = datetime('now')`,
|
|
106
|
-
[node_id, alias, alias, nodeRuntime, mdl ?? null, config_path ?? null, channels ?? null, srv ?? null, hn ?? null]
|
|
124
|
+
[node_id, nn || alias, alias, nodeRuntime, mdl ?? null, config_path ?? null, channels ?? null, srv ?? null, hn ?? null]
|
|
107
125
|
);
|
|
108
126
|
} catch {}
|
|
109
127
|
}
|
|
@@ -179,6 +197,11 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
179
197
|
}
|
|
180
198
|
|
|
181
199
|
db.run("COMMIT");
|
|
200
|
+
// Log event after commit
|
|
201
|
+
const updatedTaskId = taskUpdate.changes > 0 ? task : (db.query<{ task_id: string }, [string]>(
|
|
202
|
+
"SELECT task_id FROM tasks WHERE to_name = ?1 AND status = 'replied' ORDER BY completed_at DESC LIMIT 1"
|
|
203
|
+
).get(alias)?.task_id);
|
|
204
|
+
if (updatedTaskId) logTaskEvent(updatedTaskId, null, "replied", alias, "report_completion");
|
|
182
205
|
} catch (e) {
|
|
183
206
|
try { db.run("ROLLBACK"); } catch {}
|
|
184
207
|
throw e;
|
|
@@ -233,10 +256,11 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
233
256
|
}
|
|
234
257
|
// V2: sync tasks table — ack_inbox means delivered→acked
|
|
235
258
|
try {
|
|
236
|
-
db.run(
|
|
259
|
+
const ackResult = db.run(
|
|
237
260
|
`UPDATE tasks SET status = 'acked' WHERE task_id = ?1 AND status = 'delivered'`,
|
|
238
261
|
[message_id]
|
|
239
262
|
);
|
|
263
|
+
if (ackResult.changes > 0) logTaskEvent(message_id, "delivered", "acked", alias);
|
|
240
264
|
} catch {}
|
|
241
265
|
return {
|
|
242
266
|
content: [{ type: "text" as const, text: JSON.stringify({ ok: true }) }],
|
|
@@ -254,9 +278,11 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
254
278
|
{
|
|
255
279
|
filter_status: z.string().max(50).optional(),
|
|
256
280
|
filter_server: z.string().max(200).optional(),
|
|
281
|
+
network_id: z.string().max(200).optional().describe("Filter by network"),
|
|
257
282
|
},
|
|
258
|
-
async ({ filter_status, filter_server }) => {
|
|
259
|
-
|
|
283
|
+
async ({ filter_status, filter_server, network_id: netId }) => {
|
|
284
|
+
const effectiveNetId = getNetworkId(netId);
|
|
285
|
+
console.log(`[${ts()}] hub → get_all_status${filter_status ? ": filter=" + filter_status : ""}${effectiveNetId ? " net=" + effectiveNetId.slice(0, 12) : ""}`);
|
|
260
286
|
|
|
261
287
|
const sessions = db.transaction(() => {
|
|
262
288
|
const cutoff = new Date(Date.now() - 10 * 60 * 1000).toISOString().replace("T", " ").slice(0, 19);
|
|
@@ -264,6 +290,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
264
290
|
|
|
265
291
|
let sql = "SELECT * FROM sessions WHERE 1=1";
|
|
266
292
|
const params: any[] = [];
|
|
293
|
+
if (effectiveNetId) { sql += " AND network_id = ?"; params.push(effectiveNetId); }
|
|
267
294
|
if (filter_status) { sql += " AND status = ?"; params.push(filter_status); }
|
|
268
295
|
if (filter_server) { sql += " AND server = ?"; params.push(filter_server); }
|
|
269
296
|
sql += " ORDER BY updated_at DESC";
|
|
@@ -319,24 +346,41 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
319
346
|
priority: z.enum(["high", "normal", "low"]).optional().default("normal"),
|
|
320
347
|
context: z.string().max(10000).optional(),
|
|
321
348
|
from_session: z.string().max(200).optional().default("hub"),
|
|
349
|
+
ttl_seconds: z.number().min(1).max(86400).optional().describe("Task TTL in seconds (default: 3600)"),
|
|
350
|
+
network_id: z.string().max(200).optional().describe("Network scope"),
|
|
322
351
|
},
|
|
323
|
-
async ({ alias, task, priority, context, from_session }) => {
|
|
352
|
+
async ({ alias, task, priority, context, from_session, ttl_seconds, network_id: netId }) => {
|
|
353
|
+
const effectiveNetId = getNetworkId(netId);
|
|
354
|
+
|
|
355
|
+
// License check
|
|
356
|
+
const license = db.query<any, []>("SELECT type, expires_at FROM licenses ORDER BY created_at LIMIT 1").get();
|
|
357
|
+
if (license?.expires_at) {
|
|
358
|
+
const now = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
359
|
+
if (license.expires_at < now) {
|
|
360
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
361
|
+
ok: false, error: "license_expired",
|
|
362
|
+
message: "Trial expired. Activate a license: anet activate <key>",
|
|
363
|
+
}) }] };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
324
367
|
console.log(`[${ts()}] ${from_session} → send_task → ${alias}: ${task.slice(0, 60)}${priority === "high" ? " [HIGH]" : ""}`);
|
|
325
368
|
const id = uuidv4();
|
|
326
369
|
// 事务:inbox + tasks 双写
|
|
327
370
|
try {
|
|
328
371
|
db.run("BEGIN IMMEDIATE");
|
|
329
372
|
db.run(
|
|
330
|
-
`INSERT INTO inbox (id, session_name, type, priority, content, context, from_session, requires_response)
|
|
331
|
-
VALUES (?1, ?2, 'task', ?3, ?4, ?5, ?6, 'reply')`,
|
|
332
|
-
[id, alias, priority, task, context ?? null, from_session]
|
|
373
|
+
`INSERT INTO inbox (id, session_name, type, priority, content, context, from_session, requires_response, network_id)
|
|
374
|
+
VALUES (?1, ?2, 'task', ?3, ?4, ?5, ?6, 'reply', ?7)`,
|
|
375
|
+
[id, alias, priority, task, context ?? null, from_session, effectiveNetId]
|
|
333
376
|
);
|
|
334
377
|
db.run(
|
|
335
|
-
`INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at)
|
|
336
|
-
VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now',
|
|
337
|
-
[id, from_session, alias, priority, task]
|
|
378
|
+
`INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id)
|
|
379
|
+
VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7)`,
|
|
380
|
+
[id, from_session, alias, priority, task, `+${ttl_seconds || 3600} seconds`, effectiveNetId]
|
|
338
381
|
);
|
|
339
382
|
db.run("COMMIT");
|
|
383
|
+
logTaskEvent(id, null, "delivered", from_session, `→ ${alias}`);
|
|
340
384
|
} catch (e) {
|
|
341
385
|
try { db.run("ROLLBACK"); } catch {}
|
|
342
386
|
throw e;
|
|
@@ -415,6 +459,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
415
459
|
async ({ alias, text, in_reply_to, status: replyStatus, from_session }) => {
|
|
416
460
|
console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
|
|
417
461
|
const id = uuidv4();
|
|
462
|
+
let replyLogged = false;
|
|
418
463
|
try {
|
|
419
464
|
db.run("BEGIN IMMEDIATE");
|
|
420
465
|
db.run(
|
|
@@ -432,6 +477,8 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
432
477
|
);
|
|
433
478
|
if (result.changes === 0) {
|
|
434
479
|
console.log(`[${ts()}] ⚠ send_reply: task ${in_reply_to?.slice(0, 8)} not found or already terminal`);
|
|
480
|
+
} else {
|
|
481
|
+
replyLogged = true;
|
|
435
482
|
}
|
|
436
483
|
}
|
|
437
484
|
db.run("COMMIT");
|
|
@@ -440,6 +487,9 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
440
487
|
throw e;
|
|
441
488
|
}
|
|
442
489
|
|
|
490
|
+
// Log event after commit (outside transaction)
|
|
491
|
+
if (replyLogged && in_reply_to) logTaskEvent(in_reply_to, null, replyStatus, from_session, text.slice(0, 200));
|
|
492
|
+
|
|
443
493
|
const session = db.query<any, [string]>("SELECT status FROM sessions WHERE alias = ?1").get(alias);
|
|
444
494
|
pushEvent(alias, { type: "new_reply", from: from_session, message_id: id, in_reply_to, status: replyStatus });
|
|
445
495
|
|
|
@@ -466,6 +516,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
466
516
|
`UPDATE tasks SET status = 'acked' WHERE task_id = ?1 AND status IN ('created', 'delivered')`,
|
|
467
517
|
[task_id]
|
|
468
518
|
);
|
|
519
|
+
if (result.changes > 0) logTaskEvent(task_id, "delivered", "acked", from_session);
|
|
469
520
|
return {
|
|
470
521
|
content: [{
|
|
471
522
|
type: "text" as const,
|
|
@@ -475,6 +526,173 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
475
526
|
}
|
|
476
527
|
);
|
|
477
528
|
|
|
529
|
+
// ── V2: retry_task (重新投递失败/过期任务) ──
|
|
530
|
+
server.tool(
|
|
531
|
+
"retry_task",
|
|
532
|
+
"Retry a failed, expired, or cancelled task. Resets status to delivered and re-queues in inbox.",
|
|
533
|
+
{
|
|
534
|
+
task_id: z.string().min(1).max(200).describe("Task ID to retry"),
|
|
535
|
+
from_session: z.string().max(200).optional().default("hub"),
|
|
536
|
+
},
|
|
537
|
+
async ({ task_id, from_session }) => {
|
|
538
|
+
console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
|
|
539
|
+
// Find the original task
|
|
540
|
+
const task = db.query<any, [string]>(
|
|
541
|
+
"SELECT * FROM tasks WHERE task_id = ?1"
|
|
542
|
+
).get(task_id);
|
|
543
|
+
if (!task) {
|
|
544
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "task not found" }) }] };
|
|
545
|
+
}
|
|
546
|
+
if (!["failed", "expired", "cancelled"].includes(task.status)) {
|
|
547
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: `task status is ${task.status}, not retryable` }) }] };
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
db.run("BEGIN IMMEDIATE");
|
|
551
|
+
// Reset task status
|
|
552
|
+
db.run(
|
|
553
|
+
`UPDATE tasks SET status = 'delivered', result = NULL, completed_at = NULL, started_at = NULL, delivered_at = datetime('now'), expires_at = datetime('now', '+1 hour')
|
|
554
|
+
WHERE task_id = ?1`,
|
|
555
|
+
[task_id]
|
|
556
|
+
);
|
|
557
|
+
// Re-queue in inbox with new ID (original ID may already exist)
|
|
558
|
+
const retryInboxId = uuidv4();
|
|
559
|
+
db.run(
|
|
560
|
+
`INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response)
|
|
561
|
+
VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply')`,
|
|
562
|
+
[retryInboxId, task.to_name, task.priority, task.content, from_session]
|
|
563
|
+
);
|
|
564
|
+
db.run("COMMIT");
|
|
565
|
+
logTaskEvent(task_id, task.status, "delivered", from_session, "retry");
|
|
566
|
+
} catch (e) {
|
|
567
|
+
try { db.run("ROLLBACK"); } catch {}
|
|
568
|
+
throw e;
|
|
569
|
+
}
|
|
570
|
+
// SSE push
|
|
571
|
+
pushEvent(task.to_name, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
|
|
572
|
+
return {
|
|
573
|
+
content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, retried_to: task.to_name }) }],
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// ── V2: get_task (查询任务状态) ──
|
|
579
|
+
server.tool(
|
|
580
|
+
"get_task",
|
|
581
|
+
"Get task details by task_id. Returns status, result, timestamps.",
|
|
582
|
+
{
|
|
583
|
+
task_id: z.string().min(1).max(200).describe("Task ID to query"),
|
|
584
|
+
},
|
|
585
|
+
async ({ task_id }) => {
|
|
586
|
+
const task = db.query<any, [string]>("SELECT * FROM tasks WHERE task_id = ?1").get(task_id);
|
|
587
|
+
return {
|
|
588
|
+
content: [{
|
|
589
|
+
type: "text" as const,
|
|
590
|
+
text: JSON.stringify(task ? { ok: true, task } : { ok: false, error: "task not found" }),
|
|
591
|
+
}],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
// ── V2: list_tasks (查询任务列表) ──
|
|
597
|
+
server.tool(
|
|
598
|
+
"list_tasks",
|
|
599
|
+
"List tasks with filters. Agents can query their own pending/running tasks.",
|
|
600
|
+
{
|
|
601
|
+
alias: z.string().max(200).optional().describe("Filter by to_name (target agent)"),
|
|
602
|
+
status: z.string().max(50).optional().describe("Filter by status"),
|
|
603
|
+
from_name: z.string().max(200).optional().describe("Filter by sender"),
|
|
604
|
+
network_id: z.string().max(200).optional().describe("Filter by network"),
|
|
605
|
+
limit: z.number().min(1).max(100).optional().default(20),
|
|
606
|
+
},
|
|
607
|
+
async ({ alias, status, from_name, network_id: netId, limit }) => {
|
|
608
|
+
const effectiveNetId = getNetworkId(netId);
|
|
609
|
+
let sql = "SELECT task_id, from_name, to_name, priority, status, content, result, created_at, completed_at FROM tasks WHERE 1=1";
|
|
610
|
+
const params: any[] = [];
|
|
611
|
+
if (effectiveNetId) { sql += ` AND network_id = ?${params.length + 1}`; params.push(effectiveNetId); }
|
|
612
|
+
if (alias) { sql += ` AND to_name = ?${params.length + 1}`; params.push(alias); }
|
|
613
|
+
if (status) { sql += ` AND status = ?${params.length + 1}`; params.push(status); }
|
|
614
|
+
if (from_name) { sql += ` AND from_name = ?${params.length + 1}`; params.push(from_name); }
|
|
615
|
+
sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
|
|
616
|
+
params.push(limit);
|
|
617
|
+
const tasks = db.query(sql).all(...params);
|
|
618
|
+
|
|
619
|
+
// Stats
|
|
620
|
+
const stats = db.query<any, []>(
|
|
621
|
+
"SELECT status, COUNT(*) as count FROM tasks GROUP BY status"
|
|
622
|
+
).all();
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
content: [{
|
|
626
|
+
type: "text" as const,
|
|
627
|
+
text: JSON.stringify({ ok: true, tasks, count: tasks.length, stats }),
|
|
628
|
+
}],
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
// ── V2: cancel_task (取消任务) ──
|
|
634
|
+
server.tool(
|
|
635
|
+
"cancel_task",
|
|
636
|
+
"Cancel a pending task. Works on delivered/acked/running tasks.",
|
|
637
|
+
{
|
|
638
|
+
task_id: z.string().min(1).max(200).describe("Task ID to cancel"),
|
|
639
|
+
reason: z.string().max(1000).optional().describe("Cancellation reason"),
|
|
640
|
+
from_session: z.string().max(200).optional().default("hub"),
|
|
641
|
+
},
|
|
642
|
+
async ({ task_id, reason, from_session }) => {
|
|
643
|
+
console.log(`[${ts()}] ${from_session} → cancel_task → ${task_id.slice(0, 8)}`);
|
|
644
|
+
const result = db.run(
|
|
645
|
+
`UPDATE tasks SET status = 'cancelled', result = ?1, completed_at = datetime('now')
|
|
646
|
+
WHERE task_id = ?2 AND status IN ('created', 'delivered', 'acked', 'running')`,
|
|
647
|
+
[reason || "cancelled by " + from_session, task_id]
|
|
648
|
+
);
|
|
649
|
+
// Also ack the inbox entry to prevent agent from picking it up
|
|
650
|
+
if (result.changes > 0) {
|
|
651
|
+
db.run("UPDATE inbox SET acked = 1 WHERE id = ?1 AND acked = 0", [task_id]);
|
|
652
|
+
logTaskEvent(task_id, null, "cancelled", from_session, reason || undefined);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
content: [{ type: "text" as const, text: JSON.stringify({ ok: result.changes > 0, task_id, cancelled: result.changes > 0 }) }],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// ── V2: reassign_task (转移任务到另一个 agent) ──
|
|
661
|
+
server.tool(
|
|
662
|
+
"reassign_task",
|
|
663
|
+
"Reassign a task to a different agent. Works on any non-terminal task (delivered/acked/running).",
|
|
664
|
+
{
|
|
665
|
+
task_id: z.string().min(1).max(200).describe("Task ID to reassign"),
|
|
666
|
+
new_alias: z.string().min(1).max(200).describe("Target agent alias"),
|
|
667
|
+
from_session: z.string().max(200).optional().default("hub"),
|
|
668
|
+
},
|
|
669
|
+
async ({ task_id, new_alias, from_session }) => {
|
|
670
|
+
console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);
|
|
671
|
+
const task = db.query<any, [string]>("SELECT * FROM tasks WHERE task_id = ?1").get(task_id);
|
|
672
|
+
if (!task) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "task not found" }) }] };
|
|
673
|
+
if (["replied", "failed", "cancelled", "expired"].includes(task.status)) {
|
|
674
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: `task is terminal (${task.status})` }) }] };
|
|
675
|
+
}
|
|
676
|
+
const oldAlias = task.to_name;
|
|
677
|
+
try {
|
|
678
|
+
db.run("BEGIN IMMEDIATE");
|
|
679
|
+
// Ack old inbox to prevent original agent from picking it up
|
|
680
|
+
db.run("UPDATE inbox SET acked = 1 WHERE id = ?1 AND acked = 0", [task_id]);
|
|
681
|
+
db.run("UPDATE tasks SET to_name = ?1, status = 'delivered', started_at = NULL, delivered_at = datetime('now') WHERE task_id = ?2", [new_alias, task_id]);
|
|
682
|
+
const newInboxId = uuidv4();
|
|
683
|
+
db.run("INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response) VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply')",
|
|
684
|
+
[newInboxId, new_alias, task.priority, task.content, from_session]);
|
|
685
|
+
db.run("COMMIT");
|
|
686
|
+
logTaskEvent(task_id, task.status, "delivered", from_session, `reassign: ${oldAlias} → ${new_alias}`);
|
|
687
|
+
} catch (e) {
|
|
688
|
+
try { db.run("ROLLBACK"); } catch {}
|
|
689
|
+
throw e;
|
|
690
|
+
}
|
|
691
|
+
pushEvent(new_alias, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
|
|
692
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, reassigned_from: oldAlias, reassigned_to: new_alias }) }] };
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
|
|
478
696
|
server.tool(
|
|
479
697
|
"broadcast",
|
|
480
698
|
"Send a message to multiple sessions.",
|
|
@@ -482,11 +700,13 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
482
700
|
message: z.string().min(1).max(10000),
|
|
483
701
|
filter_server: z.string().max(200).optional(),
|
|
484
702
|
filter_status: z.string().max(50).optional(),
|
|
703
|
+
network_id: z.string().max(200).optional().describe("Broadcast within a specific network"),
|
|
485
704
|
},
|
|
486
|
-
async ({ message, filter_server, filter_status }) => {
|
|
487
|
-
console.log(`[${ts()}] hub → broadcast: ${message.slice(0, 60)}${
|
|
705
|
+
async ({ message, filter_server, filter_status, network_id: netId }) => {
|
|
706
|
+
console.log(`[${ts()}] hub → broadcast: ${message.slice(0, 60)}${netId ? " [net=" + netId.slice(0, 12) + "]" : ""}`);
|
|
488
707
|
let sql = "SELECT alias FROM sessions WHERE alias IS NOT NULL";
|
|
489
708
|
const params: any[] = [];
|
|
709
|
+
if (netId) { sql += " AND network_id = ?"; params.push(netId); }
|
|
490
710
|
if (filter_server) { sql += " AND server = ?"; params.push(filter_server); }
|
|
491
711
|
if (filter_status) { sql += " AND status = ?"; params.push(filter_status); }
|
|
492
712
|
|