@sleep2agi/commhub-server 0.5.0-preview.4 → 0.5.0-preview.6
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 +25 -0
- package/src/index.ts +19 -1
- package/src/tools.ts +59 -3
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -135,7 +135,32 @@ db.exec(`
|
|
|
135
135
|
CREATE INDEX IF NOT EXISTS idx_nodes_alias ON nodes(alias);
|
|
136
136
|
`);
|
|
137
137
|
|
|
138
|
+
// task_events table (V2 Sprint 2) — audit log for task state changes
|
|
139
|
+
db.exec(`
|
|
140
|
+
CREATE TABLE IF NOT EXISTS task_events (
|
|
141
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
142
|
+
task_id TEXT NOT NULL,
|
|
143
|
+
from_status TEXT,
|
|
144
|
+
to_status TEXT NOT NULL,
|
|
145
|
+
actor TEXT NOT NULL DEFAULT 'system',
|
|
146
|
+
detail TEXT,
|
|
147
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_task_events_task_time ON task_events(task_id, created_at DESC);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_task_events_created ON task_events(created_at);
|
|
152
|
+
`);
|
|
153
|
+
|
|
138
154
|
// Helpers
|
|
139
155
|
export function uuidv4(): string {
|
|
140
156
|
return crypto.randomUUID();
|
|
141
157
|
}
|
|
158
|
+
|
|
159
|
+
export function logTaskEvent(taskId: string, fromStatus: string | null, toStatus: string, actor: string, detail?: string) {
|
|
160
|
+
try {
|
|
161
|
+
db.run(
|
|
162
|
+
"INSERT INTO task_events (task_id, from_status, to_status, actor, detail) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
163
|
+
[taskId, fromStatus, toStatus, actor, detail ?? null]
|
|
164
|
+
);
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
4
|
import { registerTools } from "./tools.js";
|
|
5
|
-
import { db } from "./db.js";
|
|
5
|
+
import { db, logTaskEvent } from "./db.js";
|
|
6
6
|
import { createSSEStream, pushEvent, pushBroadcast, getSSEStats } from "./push.js";
|
|
7
7
|
|
|
8
8
|
const PORT = Number(process.env.PORT) || 9200;
|
|
@@ -84,6 +84,11 @@ setInterval(() => {
|
|
|
84
84
|
);
|
|
85
85
|
if (result.changes > 0) {
|
|
86
86
|
console.log(`[patrol] expired ${result.changes} stale task(s)`);
|
|
87
|
+
// Log events for expired tasks
|
|
88
|
+
const expired = db.query<{ task_id: string }, []>(
|
|
89
|
+
"SELECT task_id FROM tasks WHERE status = 'expired' AND completed_at >= datetime('now', '-1 minute')"
|
|
90
|
+
).all();
|
|
91
|
+
for (const t of expired) logTaskEvent(t.task_id, null, "expired", "patrol");
|
|
87
92
|
}
|
|
88
93
|
} catch {}
|
|
89
94
|
}, 5 * 60 * 1000);
|
|
@@ -281,6 +286,19 @@ Bun.serve({
|
|
|
281
286
|
return withCors(req, Response.json({ ok: true, messages: rows }));
|
|
282
287
|
}
|
|
283
288
|
|
|
289
|
+
// ── REST: task events (V2 Sprint 2) ──
|
|
290
|
+
if (url.pathname === "/api/task_events") {
|
|
291
|
+
const taskId = url.searchParams.get("task_id");
|
|
292
|
+
const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 500);
|
|
293
|
+
let sql = "SELECT * FROM task_events";
|
|
294
|
+
const params: any[] = [];
|
|
295
|
+
if (taskId) { sql += " WHERE task_id = ?1"; params.push(taskId); }
|
|
296
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
297
|
+
params.push(limit);
|
|
298
|
+
const rows = db.query(sql).all(...params);
|
|
299
|
+
return withCors(req, Response.json({ ok: true, events: rows, count: rows.length }));
|
|
300
|
+
}
|
|
301
|
+
|
|
284
302
|
// ── REST: nodes table (V2 Sprint 2) ──
|
|
285
303
|
if (url.pathname === "/api/nodes") {
|
|
286
304
|
const nodeId = url.searchParams.get("node_id");
|
package/src/tools.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 {
|
|
@@ -78,11 +78,18 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
78
78
|
// V2: sync tasks table — report_status(working) → tasks.running
|
|
79
79
|
if (status === "working" && task) {
|
|
80
80
|
try {
|
|
81
|
-
db.run(
|
|
81
|
+
const runResult = db.run(
|
|
82
82
|
`UPDATE tasks SET status = 'running', started_at = datetime('now')
|
|
83
83
|
WHERE to_name = ?1 AND status IN ('delivered', 'acked') AND content = ?2`,
|
|
84
84
|
[alias, task]
|
|
85
85
|
);
|
|
86
|
+
if (runResult.changes > 0) {
|
|
87
|
+
// Find task_id for logging
|
|
88
|
+
const t = db.query<{ task_id: string }, [string, string]>(
|
|
89
|
+
"SELECT task_id FROM tasks WHERE to_name = ?1 AND content = ?2 AND status = 'running' ORDER BY started_at DESC LIMIT 1"
|
|
90
|
+
).get(alias, task);
|
|
91
|
+
if (t) logTaskEvent(t.task_id, null, "running", alias);
|
|
92
|
+
}
|
|
86
93
|
} catch {}
|
|
87
94
|
}
|
|
88
95
|
|
|
@@ -180,6 +187,11 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
db.run("COMMIT");
|
|
190
|
+
// Log event after commit
|
|
191
|
+
const updatedTaskId = taskUpdate.changes > 0 ? task : (db.query<{ task_id: string }, [string]>(
|
|
192
|
+
"SELECT task_id FROM tasks WHERE to_name = ?1 AND status = 'replied' ORDER BY completed_at DESC LIMIT 1"
|
|
193
|
+
).get(alias)?.task_id);
|
|
194
|
+
if (updatedTaskId) logTaskEvent(updatedTaskId, null, "replied", alias, "report_completion");
|
|
183
195
|
} catch (e) {
|
|
184
196
|
try { db.run("ROLLBACK"); } catch {}
|
|
185
197
|
throw e;
|
|
@@ -234,10 +246,11 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
234
246
|
}
|
|
235
247
|
// V2: sync tasks table — ack_inbox means delivered→acked
|
|
236
248
|
try {
|
|
237
|
-
db.run(
|
|
249
|
+
const ackResult = db.run(
|
|
238
250
|
`UPDATE tasks SET status = 'acked' WHERE task_id = ?1 AND status = 'delivered'`,
|
|
239
251
|
[message_id]
|
|
240
252
|
);
|
|
253
|
+
if (ackResult.changes > 0) logTaskEvent(message_id, "delivered", "acked", alias);
|
|
241
254
|
} catch {}
|
|
242
255
|
return {
|
|
243
256
|
content: [{ type: "text" as const, text: JSON.stringify({ ok: true }) }],
|
|
@@ -339,6 +352,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
339
352
|
[id, from_session, alias, priority, task, `+${ttl_seconds || 3600} seconds`]
|
|
340
353
|
);
|
|
341
354
|
db.run("COMMIT");
|
|
355
|
+
logTaskEvent(id, null, "delivered", from_session, `→ ${alias}`);
|
|
342
356
|
} catch (e) {
|
|
343
357
|
try { db.run("ROLLBACK"); } catch {}
|
|
344
358
|
throw e;
|
|
@@ -417,6 +431,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
417
431
|
async ({ alias, text, in_reply_to, status: replyStatus, from_session }) => {
|
|
418
432
|
console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
|
|
419
433
|
const id = uuidv4();
|
|
434
|
+
let replyLogged = false;
|
|
420
435
|
try {
|
|
421
436
|
db.run("BEGIN IMMEDIATE");
|
|
422
437
|
db.run(
|
|
@@ -434,6 +449,8 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
434
449
|
);
|
|
435
450
|
if (result.changes === 0) {
|
|
436
451
|
console.log(`[${ts()}] ⚠ send_reply: task ${in_reply_to?.slice(0, 8)} not found or already terminal`);
|
|
452
|
+
} else {
|
|
453
|
+
replyLogged = true;
|
|
437
454
|
}
|
|
438
455
|
}
|
|
439
456
|
db.run("COMMIT");
|
|
@@ -442,6 +459,9 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
442
459
|
throw e;
|
|
443
460
|
}
|
|
444
461
|
|
|
462
|
+
// Log event after commit (outside transaction)
|
|
463
|
+
if (replyLogged && in_reply_to) logTaskEvent(in_reply_to, null, replyStatus, from_session, text.slice(0, 200));
|
|
464
|
+
|
|
445
465
|
const session = db.query<any, [string]>("SELECT status FROM sessions WHERE alias = ?1").get(alias);
|
|
446
466
|
pushEvent(alias, { type: "new_reply", from: from_session, message_id: id, in_reply_to, status: replyStatus });
|
|
447
467
|
|
|
@@ -468,6 +488,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
468
488
|
`UPDATE tasks SET status = 'acked' WHERE task_id = ?1 AND status IN ('created', 'delivered')`,
|
|
469
489
|
[task_id]
|
|
470
490
|
);
|
|
491
|
+
if (result.changes > 0) logTaskEvent(task_id, "delivered", "acked", from_session);
|
|
471
492
|
return {
|
|
472
493
|
content: [{
|
|
473
494
|
type: "text" as const,
|
|
@@ -513,6 +534,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
513
534
|
[retryInboxId, task.to_name, task.priority, task.content, from_session]
|
|
514
535
|
);
|
|
515
536
|
db.run("COMMIT");
|
|
537
|
+
logTaskEvent(task_id, task.status, "delivered", from_session, "retry");
|
|
516
538
|
} catch (e) {
|
|
517
539
|
try { db.run("ROLLBACK"); } catch {}
|
|
518
540
|
throw e;
|
|
@@ -543,6 +565,40 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
543
565
|
}
|
|
544
566
|
);
|
|
545
567
|
|
|
568
|
+
// ── V2: reassign_task (转移任务到另一个 agent) ──
|
|
569
|
+
server.tool(
|
|
570
|
+
"reassign_task",
|
|
571
|
+
"Reassign a task to a different agent. Works on any non-terminal task (delivered/acked/running).",
|
|
572
|
+
{
|
|
573
|
+
task_id: z.string().min(1).max(200).describe("Task ID to reassign"),
|
|
574
|
+
new_alias: z.string().min(1).max(200).describe("Target agent alias"),
|
|
575
|
+
from_session: z.string().max(200).optional().default("hub"),
|
|
576
|
+
},
|
|
577
|
+
async ({ task_id, new_alias, from_session }) => {
|
|
578
|
+
console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);
|
|
579
|
+
const task = db.query<any, [string]>("SELECT * FROM tasks WHERE task_id = ?1").get(task_id);
|
|
580
|
+
if (!task) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "task not found" }) }] };
|
|
581
|
+
if (["replied", "failed", "cancelled", "expired"].includes(task.status)) {
|
|
582
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: `task is terminal (${task.status})` }) }] };
|
|
583
|
+
}
|
|
584
|
+
const oldAlias = task.to_name;
|
|
585
|
+
try {
|
|
586
|
+
db.run("BEGIN IMMEDIATE");
|
|
587
|
+
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]);
|
|
588
|
+
const newInboxId = uuidv4();
|
|
589
|
+
db.run("INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response) VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply')",
|
|
590
|
+
[newInboxId, new_alias, task.priority, task.content, from_session]);
|
|
591
|
+
db.run("COMMIT");
|
|
592
|
+
logTaskEvent(task_id, task.status, "delivered", from_session, `reassign: ${oldAlias} → ${new_alias}`);
|
|
593
|
+
} catch (e) {
|
|
594
|
+
try { db.run("ROLLBACK"); } catch {}
|
|
595
|
+
throw e;
|
|
596
|
+
}
|
|
597
|
+
pushEvent(new_alias, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
|
|
598
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, reassigned_from: oldAlias, reassigned_to: new_alias }) }] };
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
|
|
546
602
|
server.tool(
|
|
547
603
|
"broadcast",
|
|
548
604
|
"Send a message to multiple sessions.",
|