@sleep2agi/commhub-server 0.5.0-preview.3 → 0.5.0-preview.5
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 +13 -0
- package/src/tools.ts +73 -2
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 ON task_events(task_id);
|
|
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
|
@@ -281,6 +281,19 @@ Bun.serve({
|
|
|
281
281
|
return withCors(req, Response.json({ ok: true, messages: rows }));
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// ── REST: task events (V2 Sprint 2) ──
|
|
285
|
+
if (url.pathname === "/api/task_events") {
|
|
286
|
+
const taskId = url.searchParams.get("task_id");
|
|
287
|
+
const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 500);
|
|
288
|
+
let sql = "SELECT * FROM task_events";
|
|
289
|
+
const params: any[] = [];
|
|
290
|
+
if (taskId) { sql += " WHERE task_id = ?1"; params.push(taskId); }
|
|
291
|
+
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
292
|
+
params.push(limit);
|
|
293
|
+
const rows = db.query(sql).all(...params);
|
|
294
|
+
return withCors(req, Response.json({ ok: true, events: rows, count: rows.length }));
|
|
295
|
+
}
|
|
296
|
+
|
|
284
297
|
// ── REST: nodes table (V2 Sprint 2) ──
|
|
285
298
|
if (url.pathname === "/api/nodes") {
|
|
286
299
|
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 {
|
|
@@ -234,10 +234,11 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
234
234
|
}
|
|
235
235
|
// V2: sync tasks table — ack_inbox means delivered→acked
|
|
236
236
|
try {
|
|
237
|
-
db.run(
|
|
237
|
+
const ackResult = db.run(
|
|
238
238
|
`UPDATE tasks SET status = 'acked' WHERE task_id = ?1 AND status = 'delivered'`,
|
|
239
239
|
[message_id]
|
|
240
240
|
);
|
|
241
|
+
if (ackResult.changes > 0) logTaskEvent(message_id, "delivered", "acked", alias);
|
|
241
242
|
} catch {}
|
|
242
243
|
return {
|
|
243
244
|
content: [{ type: "text" as const, text: JSON.stringify({ ok: true }) }],
|
|
@@ -339,6 +340,7 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
339
340
|
[id, from_session, alias, priority, task, `+${ttl_seconds || 3600} seconds`]
|
|
340
341
|
);
|
|
341
342
|
db.run("COMMIT");
|
|
343
|
+
logTaskEvent(id, null, "delivered", from_session, `→ ${alias}`);
|
|
342
344
|
} catch (e) {
|
|
343
345
|
try { db.run("ROLLBACK"); } catch {}
|
|
344
346
|
throw e;
|
|
@@ -434,6 +436,8 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
434
436
|
);
|
|
435
437
|
if (result.changes === 0) {
|
|
436
438
|
console.log(`[${ts()}] ⚠ send_reply: task ${in_reply_to?.slice(0, 8)} not found or already terminal`);
|
|
439
|
+
} else {
|
|
440
|
+
logTaskEvent(in_reply_to, null, replyStatus, from_session, text.slice(0, 200));
|
|
437
441
|
}
|
|
438
442
|
}
|
|
439
443
|
db.run("COMMIT");
|
|
@@ -477,6 +481,73 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
477
481
|
}
|
|
478
482
|
);
|
|
479
483
|
|
|
484
|
+
// ── V2: retry_task (重新投递失败/过期任务) ──
|
|
485
|
+
server.tool(
|
|
486
|
+
"retry_task",
|
|
487
|
+
"Retry a failed, expired, or cancelled task. Resets status to delivered and re-queues in inbox.",
|
|
488
|
+
{
|
|
489
|
+
task_id: z.string().min(1).max(200).describe("Task ID to retry"),
|
|
490
|
+
from_session: z.string().max(200).optional().default("hub"),
|
|
491
|
+
},
|
|
492
|
+
async ({ task_id, from_session }) => {
|
|
493
|
+
console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
|
|
494
|
+
// Find the original task
|
|
495
|
+
const task = db.query<any, [string]>(
|
|
496
|
+
"SELECT * FROM tasks WHERE task_id = ?1"
|
|
497
|
+
).get(task_id);
|
|
498
|
+
if (!task) {
|
|
499
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "task not found" }) }] };
|
|
500
|
+
}
|
|
501
|
+
if (!["failed", "expired", "cancelled"].includes(task.status)) {
|
|
502
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: `task status is ${task.status}, not retryable` }) }] };
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
db.run("BEGIN IMMEDIATE");
|
|
506
|
+
// Reset task status
|
|
507
|
+
db.run(
|
|
508
|
+
`UPDATE tasks SET status = 'delivered', result = NULL, completed_at = NULL, started_at = NULL, delivered_at = datetime('now'), expires_at = datetime('now', '+1 hour')
|
|
509
|
+
WHERE task_id = ?1`,
|
|
510
|
+
[task_id]
|
|
511
|
+
);
|
|
512
|
+
// Re-queue in inbox with new ID (original ID may already exist)
|
|
513
|
+
const retryInboxId = uuidv4();
|
|
514
|
+
db.run(
|
|
515
|
+
`INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response)
|
|
516
|
+
VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply')`,
|
|
517
|
+
[retryInboxId, task.to_name, task.priority, task.content, from_session]
|
|
518
|
+
);
|
|
519
|
+
db.run("COMMIT");
|
|
520
|
+
logTaskEvent(task_id, task.status, "delivered", from_session, "retry");
|
|
521
|
+
} catch (e) {
|
|
522
|
+
try { db.run("ROLLBACK"); } catch {}
|
|
523
|
+
throw e;
|
|
524
|
+
}
|
|
525
|
+
// SSE push
|
|
526
|
+
pushEvent(task.to_name, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
|
|
527
|
+
return {
|
|
528
|
+
content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, retried_to: task.to_name }) }],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// ── V2: get_task (查询任务状态) ──
|
|
534
|
+
server.tool(
|
|
535
|
+
"get_task",
|
|
536
|
+
"Get task details by task_id. Returns status, result, timestamps.",
|
|
537
|
+
{
|
|
538
|
+
task_id: z.string().min(1).max(200).describe("Task ID to query"),
|
|
539
|
+
},
|
|
540
|
+
async ({ task_id }) => {
|
|
541
|
+
const task = db.query<any, [string]>("SELECT * FROM tasks WHERE task_id = ?1").get(task_id);
|
|
542
|
+
return {
|
|
543
|
+
content: [{
|
|
544
|
+
type: "text" as const,
|
|
545
|
+
text: JSON.stringify(task ? { ok: true, task } : { ok: false, error: "task not found" }),
|
|
546
|
+
}],
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
|
|
480
551
|
server.tool(
|
|
481
552
|
"broadcast",
|
|
482
553
|
"Send a message to multiple sessions.",
|