@sleep2agi/commhub-server 0.8.3-preview.1 → 0.8.3-preview.2
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/index.ts +11 -8
- package/src/rename.ts +71 -0
- package/src/tools.ts +55 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sleep2agi/commhub-server",
|
|
3
|
-
"version": "0.8.3-preview.
|
|
3
|
+
"version": "0.8.3-preview.2",
|
|
4
4
|
"description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { registerTools } from "./tools.js";
|
|
|
5
5
|
import { db, logTaskEvent, logAudit } from "./db.js";
|
|
6
6
|
import { createSSEStream, pushEvent, getSSEStats } from "./push.js";
|
|
7
7
|
import { register, login, resolveToken, getUserNetworks, getUserAllNetworks, createNetwork, deleteNetwork, renameNetwork, changePassword, issueUserToken, listTokens, createToken, revokeToken, getNetworkMembers, getUserNetworkRole, addNetworkMember, updateMemberRole, removeNetworkMember, createInvite, joinByInvite, createNetworkTokenForNode, type AuthUser } from "./auth.js";
|
|
8
|
-
import {
|
|
8
|
+
import { abortRename, cleanupCommittedRenameSessions, commitRename, prepareRename, resolveCanonicalAlias } from "./rename.js";
|
|
9
9
|
|
|
10
10
|
const PORT = Number(process.env.PORT) || 9200;
|
|
11
11
|
const HOST = process.env.HOST || "127.0.0.1";
|
|
@@ -918,6 +918,7 @@ Bun.serve({
|
|
|
918
918
|
let staleSql = "UPDATE sessions SET status = 'offline' WHERE updated_at < ?1 AND status != 'offline'";
|
|
919
919
|
staleSql = addNetworkScope(staleSql, staleParams, restScope);
|
|
920
920
|
db.run(staleSql, staleParams);
|
|
921
|
+
cleanupCommittedRenameSessions(restScope.networkId ? [restScope.networkId] : restScope.networkIds ?? null);
|
|
921
922
|
const params: any[] = [];
|
|
922
923
|
let sql = "SELECT * FROM sessions WHERE 1=1";
|
|
923
924
|
sql = addNetworkScope(sql, params, restScope);
|
|
@@ -1188,6 +1189,8 @@ Bun.serve({
|
|
|
1188
1189
|
if (!canRestWriteNetwork(restAuth, taskNetId, isAdmin)) {
|
|
1189
1190
|
return withCors(req, Response.json({ ok: false, error: "permission_denied" }, { status: 403 }));
|
|
1190
1191
|
}
|
|
1192
|
+
const canonical = resolveCanonicalAlias(taskNetId, body.alias);
|
|
1193
|
+
const targetAlias = canonical.alias;
|
|
1191
1194
|
const id = crypto.randomUUID();
|
|
1192
1195
|
const fromSession = body.from || "api";
|
|
1193
1196
|
const ttlSeconds = (body as any).ttl_seconds || 3600;
|
|
@@ -1200,33 +1203,33 @@ Bun.serve({
|
|
|
1200
1203
|
db.run(
|
|
1201
1204
|
`INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response, network_id)
|
|
1202
1205
|
VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply', ?6)`,
|
|
1203
|
-
[id,
|
|
1206
|
+
[id, targetAlias, body.priority, body.task, fromSession, taskNetId]
|
|
1204
1207
|
);
|
|
1205
1208
|
db.run(
|
|
1206
1209
|
`INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id, parent_task_id)
|
|
1207
1210
|
VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7, ?8)`,
|
|
1208
|
-
[id, fromSession,
|
|
1211
|
+
[id, fromSession, targetAlias, body.priority, body.task, `+${ttlSeconds} seconds`, taskNetId, body.parent_task_id ?? null]
|
|
1209
1212
|
);
|
|
1210
1213
|
// Touch session row so the dashboard reflects "task in flight"
|
|
1211
1214
|
// immediately, without waiting for the agent's report_status to
|
|
1212
1215
|
// arrive. Updating both `task` and `updated_at` is enough — we
|
|
1213
1216
|
// leave `status` to the agent (idle → working → idle).
|
|
1214
|
-
const touchParams: any[] = [body.task.slice(0, 200),
|
|
1217
|
+
const touchParams: any[] = [body.task.slice(0, 200), targetAlias];
|
|
1215
1218
|
let touchSql = "UPDATE sessions SET task = ?1, updated_at = datetime('now') WHERE alias = ?2";
|
|
1216
1219
|
if (taskNetId) { touchSql += " AND network_id = ?3"; touchParams.push(taskNetId); }
|
|
1217
1220
|
db.run(touchSql, touchParams);
|
|
1218
1221
|
});
|
|
1219
1222
|
// SSE push: 秒达
|
|
1220
|
-
const pendingParams: any[] = [
|
|
1223
|
+
const pendingParams: any[] = [targetAlias];
|
|
1221
1224
|
let pendingSql = "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0";
|
|
1222
1225
|
if (taskNetId) { pendingSql += " AND network_id = ?2"; pendingParams.push(taskNetId); }
|
|
1223
1226
|
const pending = db.get<{ cnt: number }>(pendingSql, ...pendingParams);
|
|
1224
|
-
const sessionParams: any[] = [
|
|
1227
|
+
const sessionParams: any[] = [targetAlias];
|
|
1225
1228
|
let sessionSql = "SELECT 1 FROM sessions WHERE alias = ?1";
|
|
1226
1229
|
if (taskNetId) { sessionSql += " AND network_id = ?2"; sessionParams.push(taskNetId); }
|
|
1227
1230
|
const targetSession = db.get<any>(sessionSql, ...sessionParams);
|
|
1228
|
-
if (targetSession) pushEvent(
|
|
1229
|
-
return withCors(req, Response.json({ ok: true, task_id: id, message_id: id }));
|
|
1231
|
+
if (targetSession) pushEvent(targetAlias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority: body.priority, from: fromSession, ...(canonical.renamed ? { renamed_from: body.alias } : {}) }, taskNetId);
|
|
1232
|
+
return withCors(req, Response.json({ ok: true, task_id: id, message_id: id, ...(canonical.renamed ? { renamed_from: body.alias, renamed_to: targetAlias } : {}) }));
|
|
1230
1233
|
}
|
|
1231
1234
|
|
|
1232
1235
|
// ── REST: broadcast ──
|
package/src/rename.ts
CHANGED
|
@@ -23,11 +23,81 @@ export interface RenameResult {
|
|
|
23
23
|
error?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface CanonicalAlias {
|
|
27
|
+
alias: string;
|
|
28
|
+
renamed: boolean;
|
|
29
|
+
renamed_from?: string;
|
|
30
|
+
chain: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
function hasWriteAccess(userId: string, networkId: string): boolean {
|
|
27
34
|
const role = getUserNetworkRole(userId, networkId);
|
|
28
35
|
return !!role && role !== "viewer";
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
// Resolve committed alias renames (old -> new), following short chains such
|
|
39
|
+
// as A -> B -> C. This is intentionally server-side canonicalization: stale
|
|
40
|
+
// clients may keep reporting/sending to an old alias after commit, and the hub
|
|
41
|
+
// must not let that recreate orphan session rows (#146/#172).
|
|
42
|
+
export function resolveCanonicalAlias(networkId: string | null | undefined, alias: string): CanonicalAlias {
|
|
43
|
+
if (!networkId || !alias) return { alias, renamed: false, chain: [alias] };
|
|
44
|
+
|
|
45
|
+
const chain = [alias];
|
|
46
|
+
let current = alias;
|
|
47
|
+
const seen = new Set([alias]);
|
|
48
|
+
for (let i = 0; i < 8; i++) {
|
|
49
|
+
const row = db.get<{ new_alias: string }>(
|
|
50
|
+
"SELECT new_alias FROM rename_txn WHERE network_id = ?1 AND old_alias = ?2 AND status = 'committed' ORDER BY committed_at DESC LIMIT 1",
|
|
51
|
+
networkId, current
|
|
52
|
+
);
|
|
53
|
+
if (!row?.new_alias || seen.has(row.new_alias)) break;
|
|
54
|
+
current = row.new_alias;
|
|
55
|
+
chain.push(current);
|
|
56
|
+
seen.add(current);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
alias: current,
|
|
61
|
+
renamed: current !== alias,
|
|
62
|
+
renamed_from: current !== alias ? alias : undefined,
|
|
63
|
+
chain,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function canonicalAliasExists(networkId: string, alias: string, excludingResumeId?: string | null): boolean {
|
|
68
|
+
const params: any[] = [networkId, alias];
|
|
69
|
+
let sql = "SELECT 1 FROM sessions WHERE network_id = ?1 AND alias = ?2";
|
|
70
|
+
if (excludingResumeId) {
|
|
71
|
+
sql += " AND resume_id != ?3";
|
|
72
|
+
params.push(excludingResumeId);
|
|
73
|
+
}
|
|
74
|
+
return !!db.get<any>(sql, ...params);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function cleanupRenamedAliasSession(networkId: string | null | undefined, oldAlias: string, newAlias: string): void {
|
|
78
|
+
if (!networkId || oldAlias === newAlias) return;
|
|
79
|
+
const existsNew = db.get<any>(
|
|
80
|
+
"SELECT 1 FROM sessions WHERE network_id = ?1 AND alias = ?2",
|
|
81
|
+
networkId, newAlias
|
|
82
|
+
);
|
|
83
|
+
if (!existsNew) return;
|
|
84
|
+
db.run("DELETE FROM sessions WHERE network_id = ?1 AND alias = ?2", [networkId, oldAlias]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function cleanupCommittedRenameSessions(networkIds: string[] | null = null): void {
|
|
88
|
+
const params: any[] = [];
|
|
89
|
+
let sql = "SELECT network_id, old_alias, new_alias FROM rename_txn WHERE status = 'committed'";
|
|
90
|
+
if (networkIds) {
|
|
91
|
+
if (networkIds.length === 0) return;
|
|
92
|
+
const placeholders = networkIds.map((_, i) => `?${i + 1}`).join(", ");
|
|
93
|
+
sql += ` AND network_id IN (${placeholders})`;
|
|
94
|
+
params.push(...networkIds);
|
|
95
|
+
}
|
|
96
|
+
for (const row of db.all<{ network_id: string; old_alias: string; new_alias: string }>(sql, ...params)) {
|
|
97
|
+
cleanupRenamedAliasSession(row.network_id, row.old_alias, row.new_alias);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
31
101
|
// PHASE 1 P3 — create a prepared rename_txn row reserving new_alias.
|
|
32
102
|
// CAS guard (RFC §4 risk #3 TOCTOU): new_alias must be free both in the
|
|
33
103
|
// sessions registry AND among in-flight prepared rename_txn rows.
|
|
@@ -80,6 +150,7 @@ export function commitRename(userId: string, txnId: string): RenameResult {
|
|
|
80
150
|
db.run(
|
|
81
151
|
"UPDATE sessions SET alias = ?1, updated_at = datetime('now') WHERE network_id = ?2 AND alias = ?3",
|
|
82
152
|
[txn.new_alias, txn.network_id, txn.old_alias]);
|
|
153
|
+
cleanupRenamedAliasSession(txn.network_id, txn.old_alias, txn.new_alias);
|
|
83
154
|
db.run(
|
|
84
155
|
"UPDATE nodes SET alias = ?1, updated_at = datetime('now') WHERE network_id = ?2 AND alias = ?3",
|
|
85
156
|
[txn.new_alias, txn.network_id, txn.old_alias]);
|
package/src/tools.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod/v4";
|
|
|
3
3
|
import { db, uuidv4, logTaskEvent, chainReplyToParent } from "./db.js";
|
|
4
4
|
import { pushEvent } from "./push.js";
|
|
5
5
|
import { getUserNetworkRole } from "./auth.js";
|
|
6
|
+
import { canonicalAliasExists, cleanupRenamedAliasSession, resolveCanonicalAlias } from "./rename.js";
|
|
6
7
|
|
|
7
8
|
function ts(): string {
|
|
8
9
|
return new Date().toTimeString().slice(0, 8);
|
|
@@ -149,7 +150,36 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
149
150
|
if (!canWrite(effectiveNetId)) {
|
|
150
151
|
return writeDeniedReply(effectiveNetId);
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
const canonical = resolveCanonicalAlias(sessionNetId, alias);
|
|
154
|
+
let effectiveAlias = canonical.alias;
|
|
155
|
+
if (canonical.renamed) {
|
|
156
|
+
// A stale process may keep heartbeating with the old alias after a
|
|
157
|
+
// committed rename. If the new alias is already active, ignore the
|
|
158
|
+
// stale report and clean the old row instead of letting it recreate
|
|
159
|
+
// a red/orphan dashboard node (#146/#172). If not active yet, rewrite
|
|
160
|
+
// the incoming report to the canonical alias so startup can converge.
|
|
161
|
+
if (canonicalAliasExists(sessionNetId, effectiveAlias, resume_id)) {
|
|
162
|
+
cleanupRenamedAliasSession(sessionNetId, alias, effectiveAlias);
|
|
163
|
+
const pendingParams: any[] = [effectiveAlias];
|
|
164
|
+
let pendingSql = "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0";
|
|
165
|
+
pendingSql = addScope(pendingSql, pendingParams, effectiveNetId);
|
|
166
|
+
const pending = db.get<{ cnt: number }>(pendingSql, ...pendingParams);
|
|
167
|
+
return {
|
|
168
|
+
content: [{
|
|
169
|
+
type: "text" as const,
|
|
170
|
+
text: JSON.stringify({
|
|
171
|
+
ok: true,
|
|
172
|
+
resume_id,
|
|
173
|
+
alias: effectiveAlias,
|
|
174
|
+
renamed_from: alias,
|
|
175
|
+
ignored_stale_alias: true,
|
|
176
|
+
inbox_count: pending?.cnt ?? 0,
|
|
177
|
+
}),
|
|
178
|
+
}],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
console.log(`[${ts()}] ${effectiveAlias} (${resume_id.slice(0, 8)}) → report_status: ${status}${task ? " | " + task.slice(0, 60) : ""}${effectiveNetId ? " [net]" : ""}${canonical.renamed ? ` [renamed from ${alias}]` : ""}`);
|
|
153
183
|
const trimmedOutput = output?.slice(0, 4000);
|
|
154
184
|
const hostHostname = host?.hostname || hn || null;
|
|
155
185
|
const hostIp = host?.ip || clientIP || null;
|
|
@@ -190,7 +220,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
190
220
|
|
|
191
221
|
db.transaction(() => {
|
|
192
222
|
// Only delete same-alias sessions within the same network
|
|
193
|
-
db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2 AND network_id = ?3", [
|
|
223
|
+
db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2 AND network_id = ?3", [effectiveAlias, resume_id, sessionNetId]);
|
|
194
224
|
db.run(
|
|
195
225
|
`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, model, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb, process_rss_bytes, process_rss_mb, process_cpu_pct, process_uptime_seconds, process_in_flight_count, last_seen_at, updated_at)
|
|
196
226
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, datetime('now'), datetime('now'))
|
|
@@ -219,19 +249,20 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
219
249
|
process_uptime_seconds = COALESCE(?32, sessions.process_uptime_seconds),
|
|
220
250
|
process_in_flight_count = COALESCE(?33, sessions.process_in_flight_count),
|
|
221
251
|
last_seen_at = datetime('now'), updated_at = datetime('now')`,
|
|
222
|
-
[resume_id,
|
|
252
|
+
[resume_id, effectiveAlias, tmux ?? null, srv ?? null, hostIp, hostHostname, 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, sessionNetId, mdl ?? null, cpuLoad1m, cpuCores, memTotalGb, memUsedGb, memAvailGb, diskTotalGb, diskUsedGb, diskAvailGb, processRssBytes, processRssMb, processCpuPct, processUptimeSeconds, processInFlightCount]
|
|
223
253
|
);
|
|
224
254
|
if (host || proc) {
|
|
225
255
|
db.run(
|
|
226
256
|
`INSERT INTO agent_telemetry (id, network_id, resume_id, alias, hostname, ip, cpu_load_1min, cpu_cores, mem_total_gb, mem_used_gb, mem_avail_gb, disk_total_gb, disk_used_gb, disk_avail_gb, process_rss_bytes, process_rss_mb, process_cpu_pct, process_uptime_seconds, process_in_flight_count, created_at)
|
|
227
257
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, datetime('now'))`,
|
|
228
|
-
[uuidv4(), sessionNetId, resume_id,
|
|
258
|
+
[uuidv4(), sessionNetId, resume_id, effectiveAlias, hostHostname, hostIp, cpuLoad1m, cpuCores, memTotalGb, memUsedGb, memAvailGb, diskTotalGb, diskUsedGb, diskAvailGb, processRssBytes, processRssMb, processCpuPct, processUptimeSeconds, processInFlightCount]
|
|
229
259
|
);
|
|
230
260
|
}
|
|
231
261
|
});
|
|
232
|
-
pushEvent(
|
|
262
|
+
pushEvent(effectiveAlias, {
|
|
233
263
|
type: "status_update",
|
|
234
|
-
alias,
|
|
264
|
+
alias: effectiveAlias,
|
|
265
|
+
...(canonical.renamed ? { renamed_from: alias } : {}),
|
|
235
266
|
status,
|
|
236
267
|
progress: progress ?? null,
|
|
237
268
|
host: statusHostTelemetry,
|
|
@@ -241,19 +272,19 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
241
272
|
// V2: sync tasks table — report_status(working) → tasks.running
|
|
242
273
|
if (status === "working" && task) {
|
|
243
274
|
try {
|
|
244
|
-
const runParams: any[] = [
|
|
275
|
+
const runParams: any[] = [effectiveAlias, task];
|
|
245
276
|
let runSql = `UPDATE tasks SET status = 'running', started_at = datetime('now')
|
|
246
277
|
WHERE to_name = ?1 AND status IN ('delivered', 'acked') AND content = ?2`;
|
|
247
278
|
runSql = addScope(runSql, runParams, effectiveNetId);
|
|
248
279
|
const runResult = db.run(runSql, runParams);
|
|
249
280
|
if (runResult.changes > 0) {
|
|
250
281
|
// Find task_id for logging
|
|
251
|
-
const findParams: any[] = [
|
|
282
|
+
const findParams: any[] = [effectiveAlias, task];
|
|
252
283
|
let findSql = "SELECT task_id FROM tasks WHERE to_name = ?1 AND content = ?2 AND status = 'running'";
|
|
253
284
|
findSql = addScope(findSql, findParams, effectiveNetId);
|
|
254
285
|
findSql += " ORDER BY started_at DESC LIMIT 1";
|
|
255
286
|
const t = db.get<{ task_id: string }>(findSql, ...findParams);
|
|
256
|
-
if (t) logTaskEvent(t.task_id, null, "running",
|
|
287
|
+
if (t) logTaskEvent(t.task_id, null, "running", effectiveAlias);
|
|
257
288
|
}
|
|
258
289
|
} catch {}
|
|
259
290
|
}
|
|
@@ -277,13 +308,13 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
277
308
|
hostname = COALESCE(?9, nodes.hostname),
|
|
278
309
|
network_id = COALESCE(?10, nodes.network_id),
|
|
279
310
|
updated_at = datetime('now')`,
|
|
280
|
-
[node_id, nn ||
|
|
311
|
+
[node_id, nn || effectiveAlias, effectiveAlias, nodeRuntime, mdl ?? null, config_path ?? null, channels ?? null, srv ?? null, hn ?? null, effectiveNetId ?? null]
|
|
281
312
|
);
|
|
282
313
|
} catch {}
|
|
283
314
|
}
|
|
284
315
|
|
|
285
316
|
// inbox uses alias for routing
|
|
286
|
-
const inboxParams: any[] = [
|
|
317
|
+
const inboxParams: any[] = [effectiveAlias];
|
|
287
318
|
let inboxSql = "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0";
|
|
288
319
|
inboxSql = addScope(inboxSql, inboxParams, effectiveNetId);
|
|
289
320
|
const row = db.get<{ cnt: number }>(inboxSql, ...inboxParams);
|
|
@@ -295,7 +326,8 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
295
326
|
text: JSON.stringify({
|
|
296
327
|
ok: true,
|
|
297
328
|
resume_id,
|
|
298
|
-
alias,
|
|
329
|
+
alias: effectiveAlias,
|
|
330
|
+
...(canonical.renamed ? { renamed_from: alias } : {}),
|
|
299
331
|
inbox_count: row?.cnt ?? 0,
|
|
300
332
|
}),
|
|
301
333
|
},
|
|
@@ -587,7 +619,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
587
619
|
}
|
|
588
620
|
}
|
|
589
621
|
|
|
590
|
-
|
|
622
|
+
const canonical = resolveCanonicalAlias(effectiveNetId, alias);
|
|
623
|
+
const targetAlias = canonical.alias;
|
|
624
|
+
console.log(`[${ts()}] ${from_session} → send_task → ${targetAlias}: ${task.slice(0, 60)}${priority === "high" ? " [HIGH]" : ""}${canonical.renamed ? ` [renamed from ${alias}]` : ""}`);
|
|
591
625
|
const id = uuidv4();
|
|
592
626
|
// 事务:inbox + tasks 双写 + 触碰目标 session 的 task/updated_at(让
|
|
593
627
|
// dashboard 在派任务一刻就反映出"任务已下发",不再等 agent 的
|
|
@@ -597,21 +631,21 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
597
631
|
db.run(
|
|
598
632
|
`INSERT INTO inbox (id, session_name, type, priority, content, context, from_session, requires_response, network_id)
|
|
599
633
|
VALUES (?1, ?2, 'task', ?3, ?4, ?5, ?6, 'reply', ?7)`,
|
|
600
|
-
[id,
|
|
634
|
+
[id, targetAlias, priority, task, context ?? null, from_session, effectiveNetId ?? null]
|
|
601
635
|
);
|
|
602
636
|
db.run(
|
|
603
637
|
`INSERT INTO tasks (task_id, from_name, to_name, priority, status, content, requires_response, created_at, delivered_at, expires_at, network_id, parent_task_id)
|
|
604
638
|
VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7, ?8)`,
|
|
605
|
-
[id, from_session,
|
|
639
|
+
[id, from_session, targetAlias, priority, task, `+${ttl_seconds || 3600} seconds`, effectiveNetId ?? null, parentTaskId]
|
|
606
640
|
);
|
|
607
|
-
const touchParams: any[] = [task.slice(0, 200),
|
|
641
|
+
const touchParams: any[] = [task.slice(0, 200), targetAlias];
|
|
608
642
|
let touchSql = "UPDATE sessions SET task = ?1, updated_at = datetime('now') WHERE alias = ?2";
|
|
609
643
|
touchSql = addScope(touchSql, touchParams, effectiveNetId);
|
|
610
644
|
db.run(touchSql, touchParams);
|
|
611
645
|
});
|
|
612
|
-
logTaskEvent(id, null, "delivered", from_session, parentTaskId ? `→ ${
|
|
646
|
+
logTaskEvent(id, null, "delivered", from_session, parentTaskId ? `→ ${targetAlias} (parent=${parentTaskId.slice(0,8)})` : `→ ${targetAlias}`);
|
|
613
647
|
|
|
614
|
-
const session = scopedSessionStatus(
|
|
648
|
+
const session = scopedSessionStatus(targetAlias, effectiveNetId);
|
|
615
649
|
|
|
616
650
|
// SSE push by alias.
|
|
617
651
|
// The SSE channel is keyed by alias (subscribers connected to /events/<alias>),
|
|
@@ -620,11 +654,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
620
654
|
// network_id=null but the sender supplied an explicit network_id (the
|
|
621
655
|
// exact mismatch hit by Dashboard tasks). Push unconditionally; the
|
|
622
656
|
// subscriber's own auth (ntok_) constrains who can listen.
|
|
623
|
-
const pendingParams: any[] = [
|
|
657
|
+
const pendingParams: any[] = [targetAlias];
|
|
624
658
|
let pendingSql = "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0";
|
|
625
659
|
pendingSql = addScope(pendingSql, pendingParams, effectiveNetId);
|
|
626
660
|
const pending = db.get<{ cnt: number }>(pendingSql, ...pendingParams);
|
|
627
|
-
pushEvent(
|
|
661
|
+
pushEvent(targetAlias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority, from: from_session, ...(canonical.renamed ? { renamed_from: alias } : {}) }, effectiveNetId);
|
|
628
662
|
|
|
629
663
|
return {
|
|
630
664
|
content: [
|
|
@@ -633,6 +667,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
633
667
|
text: JSON.stringify({
|
|
634
668
|
ok: true,
|
|
635
669
|
message_id: id,
|
|
670
|
+
...(canonical.renamed ? { renamed_from: alias, renamed_to: targetAlias } : {}),
|
|
636
671
|
session_status: session?.status ?? "unknown",
|
|
637
672
|
}),
|
|
638
673
|
},
|