@adhdev/daemon-standalone 0.9.82-rc.19 → 0.9.82-rc.191
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/dist/index.js +57207 -27737
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/public/assets/index-D48dS0zi.js +105 -0
- package/public/assets/index-nXxZVWAP.css +1 -0
- package/public/assets/{terminal-CfAvHQvJ.js → terminal-DN8I8rbd.js} +4 -4
- package/public/assets/vendor-BwuWgaJI.js +2768 -0
- package/public/index.html +3 -3
- package/public/snake.html +229 -0
- package/vendor/mcp-server/index.js +1444 -192
- package/vendor/mcp-server/index.js.map +1 -1
- package/vendor/mcp-server/package.json +1 -1
- package/vendor/session-host-daemon/index.js +78 -2
- package/vendor/session-host-daemon/index.js.map +1 -1
- package/vendor/session-host-daemon/index.mjs +78 -2
- package/vendor/session-host-daemon/index.mjs.map +1 -1
- package/public/assets/index-CsEmn20l.css +0 -1
- package/public/assets/index-Deb1Bk0M.js +0 -98
- package/public/assets/vendor-CgiI0UIA.js +0 -2745
|
@@ -35,9 +35,128 @@ __export(index_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
|
+
// src/tools/mesh-tools.ts
|
|
39
|
+
var import_node_crypto = require("crypto");
|
|
40
|
+
|
|
38
41
|
// src/transports/ipc.ts
|
|
39
42
|
var DEFAULT_IPC_PORT = 19222;
|
|
40
43
|
var DEFAULT_IPC_PATH = "/ipc";
|
|
44
|
+
var DEFAULT_IPC_COMMAND_TIMEOUT_MS = 15e3;
|
|
45
|
+
var IPC_COMMAND_TIMEOUTS_MS = {
|
|
46
|
+
mesh_relay_command: 12e4,
|
|
47
|
+
agent_command: 3e4,
|
|
48
|
+
git_status: 45e3,
|
|
49
|
+
git_diff_summary: 45e3,
|
|
50
|
+
fast_forward_mesh_node: 12e4,
|
|
51
|
+
mesh_status: 12e4
|
|
52
|
+
};
|
|
53
|
+
var WS_CONNECTING = 0;
|
|
54
|
+
var WS_OPEN = 1;
|
|
55
|
+
var POOL_IDLE_EVICT_MS = 5 * 6e4;
|
|
56
|
+
var POOL_MAX_AGE_MS = 10 * 6e4;
|
|
57
|
+
var connectionPool = /* @__PURE__ */ new Map();
|
|
58
|
+
function buildRequestId() {
|
|
59
|
+
return `mcp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
60
|
+
}
|
|
61
|
+
function getTimeoutMs(type, nestedCommand) {
|
|
62
|
+
return Math.max(
|
|
63
|
+
IPC_COMMAND_TIMEOUTS_MS[type] ?? DEFAULT_IPC_COMMAND_TIMEOUT_MS,
|
|
64
|
+
IPC_COMMAND_TIMEOUTS_MS[nestedCommand] ?? DEFAULT_IPC_COMMAND_TIMEOUT_MS
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function getOrCreateConnection(WebSocketCtor, url) {
|
|
68
|
+
const existing = connectionPool.get(url);
|
|
69
|
+
if (existing) {
|
|
70
|
+
const { readyState } = existing.ws;
|
|
71
|
+
const now2 = Date.now();
|
|
72
|
+
const isAlive = readyState === WS_CONNECTING || readyState === WS_OPEN;
|
|
73
|
+
const isIdle = now2 - existing.lastUsedAt > POOL_IDLE_EVICT_MS && existing.pending.size === 0;
|
|
74
|
+
const isTooOld = now2 - existing.createdAt > POOL_MAX_AGE_MS && existing.pending.size === 0;
|
|
75
|
+
if (isAlive && !isIdle && !isTooOld) {
|
|
76
|
+
return existing;
|
|
77
|
+
}
|
|
78
|
+
if (isAlive && (isIdle || isTooOld)) {
|
|
79
|
+
try {
|
|
80
|
+
existing.ws.close();
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
connectionPool.delete(url);
|
|
84
|
+
}
|
|
85
|
+
connectionPool.delete(url);
|
|
86
|
+
}
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const conn = {
|
|
89
|
+
ws: new WebSocketCtor(url),
|
|
90
|
+
ready: false,
|
|
91
|
+
commandQueue: [],
|
|
92
|
+
pending: /* @__PURE__ */ new Map(),
|
|
93
|
+
lastUsedAt: now,
|
|
94
|
+
createdAt: now
|
|
95
|
+
};
|
|
96
|
+
connectionPool.set(url, conn);
|
|
97
|
+
const drainQueue = () => {
|
|
98
|
+
conn.ready = true;
|
|
99
|
+
for (const { type, args, requestId } of conn.commandQueue) {
|
|
100
|
+
conn.ws.send(JSON.stringify({ type: "ext:command", payload: { command: type, args, requestId } }));
|
|
101
|
+
}
|
|
102
|
+
conn.commandQueue = [];
|
|
103
|
+
};
|
|
104
|
+
let tornDown = false;
|
|
105
|
+
const teardown = (error) => {
|
|
106
|
+
if (tornDown) return;
|
|
107
|
+
tornDown = true;
|
|
108
|
+
connectionPool.delete(url);
|
|
109
|
+
conn.ready = false;
|
|
110
|
+
for (const [, req] of conn.pending) {
|
|
111
|
+
clearTimeout(req.timer);
|
|
112
|
+
req.reject(error);
|
|
113
|
+
}
|
|
114
|
+
conn.pending.clear();
|
|
115
|
+
conn.commandQueue = [];
|
|
116
|
+
};
|
|
117
|
+
conn.ws.addEventListener("open", () => {
|
|
118
|
+
conn.ws.send(JSON.stringify({
|
|
119
|
+
type: "ext:register",
|
|
120
|
+
payload: {
|
|
121
|
+
ideType: "mcp-server",
|
|
122
|
+
ideVersion: "1.0.0",
|
|
123
|
+
extensionVersion: "1.0.0",
|
|
124
|
+
instanceId: `mcp-server-${process.pid}`,
|
|
125
|
+
machineId: "mcp-server",
|
|
126
|
+
workspaceFolders: []
|
|
127
|
+
}
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
130
|
+
conn.ws.addEventListener("message", (event) => {
|
|
131
|
+
try {
|
|
132
|
+
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
133
|
+
const msg = JSON.parse(raw);
|
|
134
|
+
if (msg?.type === "daemon:welcome") {
|
|
135
|
+
drainQueue();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (msg?.type !== "ext:command_result") return;
|
|
139
|
+
const req = conn.pending.get(msg?.payload?.requestId);
|
|
140
|
+
if (!req) return;
|
|
141
|
+
conn.pending.delete(msg.payload.requestId);
|
|
142
|
+
clearTimeout(req.timer);
|
|
143
|
+
const payload = msg.payload;
|
|
144
|
+
if (payload?.success === false) {
|
|
145
|
+
req.reject(new Error(payload.error || "Daemon IPC command failed"));
|
|
146
|
+
} else {
|
|
147
|
+
req.resolve(payload?.result ?? payload);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
conn.ws.addEventListener("error", () => {
|
|
153
|
+
teardown(new Error(`Cannot connect to daemon IPC at ${url}`));
|
|
154
|
+
});
|
|
155
|
+
conn.ws.addEventListener("close", () => {
|
|
156
|
+
teardown(new Error(`Daemon IPC connection closed: ${url}`));
|
|
157
|
+
});
|
|
158
|
+
return conn;
|
|
159
|
+
}
|
|
41
160
|
var IpcTransport = class {
|
|
42
161
|
port;
|
|
43
162
|
path;
|
|
@@ -66,73 +185,41 @@ var IpcTransport = class {
|
|
|
66
185
|
args
|
|
67
186
|
});
|
|
68
187
|
}
|
|
69
|
-
|
|
188
|
+
sendIpcCommand(type, args) {
|
|
70
189
|
const WebSocketCtor = globalThis.WebSocket;
|
|
71
190
|
if (!WebSocketCtor) {
|
|
72
|
-
|
|
191
|
+
return Promise.reject(new Error("WebSocket is not available in this Node runtime; Node 20+ is required for daemon IPC mode"));
|
|
73
192
|
}
|
|
193
|
+
const requestId = buildRequestId();
|
|
194
|
+
const nestedCommand = typeof args?.command === "string" ? args.command : "";
|
|
195
|
+
const timeoutMs = getTimeoutMs(type, nestedCommand);
|
|
196
|
+
const targetDaemonId = typeof args?.targetDaemonId === "string" ? args.targetDaemonId : "";
|
|
197
|
+
const diagnosticParts = [
|
|
198
|
+
`command='${type}'`,
|
|
199
|
+
...nestedCommand ? [`relayedCommand='${nestedCommand}'`] : [],
|
|
200
|
+
...targetDaemonId ? [`targetDaemonId='${targetDaemonId.slice(0, 12)}'`] : [],
|
|
201
|
+
...typeof args?.nodeId === "string" ? [`nodeId='${args.nodeId}'`] : [],
|
|
202
|
+
...typeof args?.workspace === "string" ? [`workspace='${args.workspace}'`] : []
|
|
203
|
+
];
|
|
204
|
+
const url = `ws://127.0.0.1:${this.port}${this.path}`;
|
|
74
205
|
return new Promise((resolve, reject) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} catch {
|
|
85
|
-
}
|
|
86
|
-
fn();
|
|
87
|
-
};
|
|
88
|
-
const timeoutMs = type === "mesh_relay_command" ? 6e4 : 15e3;
|
|
89
|
-
const timeout = setTimeout(() => {
|
|
90
|
-
finish(() => reject(new Error(`Daemon IPC command '${type}' timed out after ${Math.round(timeoutMs / 1e3)}s`)));
|
|
206
|
+
let conn;
|
|
207
|
+
try {
|
|
208
|
+
conn = getOrCreateConnection(WebSocketCtor, url);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return reject(new Error(`Failed to create IPC connection: ${e?.message || e}`));
|
|
211
|
+
}
|
|
212
|
+
const timer = setTimeout(() => {
|
|
213
|
+
conn.pending.delete(requestId);
|
|
214
|
+
reject(new Error(`Daemon IPC ${diagnosticParts.join(" ")} timed out after ${Math.round(timeoutMs / 1e3)}s (requestId=${requestId})`));
|
|
91
215
|
}, timeoutMs);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}));
|
|
100
|
-
};
|
|
101
|
-
ws.addEventListener("open", () => {
|
|
102
|
-
ws.send(JSON.stringify({
|
|
103
|
-
type: "ext:register",
|
|
104
|
-
payload: {
|
|
105
|
-
ideType: "mcp-server",
|
|
106
|
-
ideVersion: "1.0.0",
|
|
107
|
-
extensionVersion: "1.0.0",
|
|
108
|
-
instanceId: `mcp-server-${process.pid}`,
|
|
109
|
-
machineId: "mcp-server",
|
|
110
|
-
workspaceFolders: []
|
|
111
|
-
}
|
|
112
|
-
}));
|
|
113
|
-
});
|
|
114
|
-
ws.addEventListener("message", (event) => {
|
|
115
|
-
try {
|
|
116
|
-
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
117
|
-
const msg = JSON.parse(raw);
|
|
118
|
-
if (msg?.type === "daemon:welcome") {
|
|
119
|
-
send();
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (msg?.type !== "ext:command_result") return;
|
|
123
|
-
if (msg?.payload?.requestId !== requestId) return;
|
|
124
|
-
const payload = msg.payload;
|
|
125
|
-
if (payload?.success === false) {
|
|
126
|
-
finish(() => reject(new Error(payload.error || `Daemon IPC command '${type}' failed`)));
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
finish(() => resolve(payload?.result ?? payload));
|
|
130
|
-
} catch {
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
ws.addEventListener("error", () => {
|
|
134
|
-
finish(() => reject(new Error(`Cannot connect to daemon IPC at ws://127.0.0.1:${this.port}${this.path}`)));
|
|
135
|
-
});
|
|
216
|
+
conn.pending.set(requestId, { resolve, reject, timer });
|
|
217
|
+
conn.lastUsedAt = Date.now();
|
|
218
|
+
if (conn.ready) {
|
|
219
|
+
conn.ws.send(JSON.stringify({ type: "ext:command", payload: { command: type, args, requestId } }));
|
|
220
|
+
} else {
|
|
221
|
+
conn.commandQueue.push({ type, args, requestId });
|
|
222
|
+
}
|
|
136
223
|
});
|
|
137
224
|
}
|
|
138
225
|
};
|
|
@@ -143,10 +230,6 @@ function isLocalTransport(transport) {
|
|
|
143
230
|
}
|
|
144
231
|
|
|
145
232
|
// src/tools/chat-compact.ts
|
|
146
|
-
function isAssistantLike(message) {
|
|
147
|
-
const role = String(message?.role ?? "").toLowerCase();
|
|
148
|
-
return role === "assistant" || role === "agent";
|
|
149
|
-
}
|
|
150
233
|
function messageContent(message) {
|
|
151
234
|
const content = message?.content;
|
|
152
235
|
if (typeof content === "string") return content;
|
|
@@ -166,10 +249,7 @@ function isCoordinatorVisibleMessage(message) {
|
|
|
166
249
|
return role === "user" || role === "assistant" || role === "agent";
|
|
167
250
|
}
|
|
168
251
|
function buildCompactMessageTail(visibleMessages, opts) {
|
|
169
|
-
|
|
170
|
-
const shouldOmitSummaryMessage = !!summary && !!opts.finalAssistant && isAssistantLike(opts.finalAssistant) && messageContent(opts.finalAssistant).trim() === summary;
|
|
171
|
-
const sourceMessages = shouldOmitSummaryMessage ? visibleMessages.filter((message) => message !== opts.finalAssistant) : visibleMessages;
|
|
172
|
-
return sourceMessages.slice(-opts.limit);
|
|
252
|
+
return visibleMessages.slice(-opts.limit);
|
|
173
253
|
}
|
|
174
254
|
function compactChatPayload(payload, opts = {}) {
|
|
175
255
|
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
@@ -241,17 +321,64 @@ function annotateRapidReadChatAdvisory(payload, options) {
|
|
|
241
321
|
|
|
242
322
|
// src/tools/mesh-tools.ts
|
|
243
323
|
var import_daemon_core = require("@adhdev/daemon-core");
|
|
324
|
+
var SESSION_PROVIDER_METADATA_TTL_MS = 30 * 6e4;
|
|
244
325
|
var meshSessionProviderMetadata = /* @__PURE__ */ new Map();
|
|
326
|
+
function getSessionMetadata(key) {
|
|
327
|
+
const entry = meshSessionProviderMetadata.get(key);
|
|
328
|
+
if (!entry) return void 0;
|
|
329
|
+
if (entry.expiresAt <= Date.now()) {
|
|
330
|
+
meshSessionProviderMetadata.delete(key);
|
|
331
|
+
return void 0;
|
|
332
|
+
}
|
|
333
|
+
return entry;
|
|
334
|
+
}
|
|
335
|
+
var ACTIVE_WORK_POLLING_BACKOFF_MS = 6e4;
|
|
336
|
+
function buildActiveWorkPollingGuidance(summary, now = Date.now()) {
|
|
337
|
+
if (!summary || summary.generatingCount <= 0) return void 0;
|
|
338
|
+
return {
|
|
339
|
+
activeGeneratingWork: true,
|
|
340
|
+
generatingCount: summary.generatingCount,
|
|
341
|
+
doNotPollBefore: new Date(now + ACTIVE_WORK_POLLING_BACKOFF_MS).toISOString(),
|
|
342
|
+
eventSurface: "pendingCoordinatorEvents",
|
|
343
|
+
nextRecommendedAction: "Wait for pendingCoordinatorEvents/completion events or an explicit user status request. If no terminal evidence appears and the user asks for status, make one bounded status check, then wait again.",
|
|
344
|
+
message: "Do not repeatedly poll mesh_status/mesh_view_queue/mesh_read_chat while delegated work is generating; terminal ledger or completion evidence will be surfaced through pendingCoordinatorEvents when available."
|
|
345
|
+
};
|
|
346
|
+
}
|
|
245
347
|
function readString(value) {
|
|
246
348
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
247
349
|
}
|
|
350
|
+
function summarizeTaskMessage(message) {
|
|
351
|
+
const taskSummary = message.replace(/\s+/g, " ").trim();
|
|
352
|
+
const taskTitle = taskSummary.length > 96 ? `${taskSummary.slice(0, 93)}...` : taskSummary;
|
|
353
|
+
return { taskTitle: taskTitle || "(untitled task)", taskSummary };
|
|
354
|
+
}
|
|
355
|
+
function buildDirectTaskPayload(message, via, opts) {
|
|
356
|
+
const descriptor = summarizeTaskMessage(message);
|
|
357
|
+
return {
|
|
358
|
+
source: "direct",
|
|
359
|
+
via,
|
|
360
|
+
taskId: opts.taskId,
|
|
361
|
+
message,
|
|
362
|
+
taskTitle: descriptor.taskTitle,
|
|
363
|
+
taskSummary: descriptor.taskSummary,
|
|
364
|
+
...opts.taskMode ? { taskMode: opts.taskMode } : {},
|
|
365
|
+
...opts.providerType ? { providerType: opts.providerType } : {},
|
|
366
|
+
...opts.targetSessionId ? { targetSessionId: opts.targetSessionId } : {},
|
|
367
|
+
...opts.dispatchedToIdleSession !== void 0 ? { dispatchedToIdleSession: opts.dispatchedToIdleSession } : {}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function findNode(mesh, nodeId) {
|
|
371
|
+
const node = mesh.nodes.find((n) => n.id === nodeId);
|
|
372
|
+
if (!node) throw new Error(`Node '${nodeId}' is not a member of mesh '${mesh.name}'`);
|
|
373
|
+
return node;
|
|
374
|
+
}
|
|
248
375
|
var DUPLICATE_DISPATCH_WINDOW_MS = 6e4;
|
|
249
376
|
var STALE_ASSIGNED_QUEUE_MS = 30 * 6e4;
|
|
250
377
|
var OLD_HISTORICAL_QUEUE_RECORD_MS = 7 * 24 * 60 * 6e4;
|
|
251
378
|
var ACTIVE_QUEUE_STATUSES = /* @__PURE__ */ new Set(["pending", "assigned"]);
|
|
252
379
|
var HISTORICAL_QUEUE_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
253
380
|
async function refreshMeshFromDaemon(ctx) {
|
|
254
|
-
if (!(ctx.transport
|
|
381
|
+
if (!isLocalTransport(ctx.transport)) return;
|
|
255
382
|
try {
|
|
256
383
|
const result = await ctx.transport.command("get_mesh", { meshId: ctx.mesh.id });
|
|
257
384
|
if (!result?.success || !Array.isArray(result.mesh?.nodes)) return;
|
|
@@ -404,6 +531,33 @@ function buildMissingNodeReadChatRecovery(ctx, args) {
|
|
|
404
531
|
function readSessionRecordId(session) {
|
|
405
532
|
return readString(session?.id) || readString(session?.sessionId) || readString(session?.session_id) || readString(session?.runtimeSessionId) || readString(session?.runtime_session_id) || readString(session?.instanceId) || readString(session?.instance_id);
|
|
406
533
|
}
|
|
534
|
+
function extractStatusMetadataSessions(value) {
|
|
535
|
+
const payload = unwrapCommandPayload(value);
|
|
536
|
+
const status = payload?.status && typeof payload.status === "object" ? payload.status : payload;
|
|
537
|
+
return Array.isArray(status?.sessions) ? status.sessions : [];
|
|
538
|
+
}
|
|
539
|
+
function resolveSessionProviderType(session) {
|
|
540
|
+
return readString(session?.providerType) || readString(session?.cliType) || readString(session?.agentType) || "";
|
|
541
|
+
}
|
|
542
|
+
function isMeshCoordinatorSessionRecord(session) {
|
|
543
|
+
return Boolean(
|
|
544
|
+
readString(session?.settings?.meshCoordinatorFor) || readString(session?.meta?.meshCoordinatorFor) || readString(session?.metadata?.meshCoordinatorFor) || readString(session?.meshCoordinatorFor)
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
function isUnmanagedSessionRecord(session) {
|
|
548
|
+
const hasMeshNodeFor = Boolean(
|
|
549
|
+
readString(session?.settings?.meshNodeFor) || readString(session?.meta?.meshNodeFor) || readString(session?.metadata?.meshNodeFor) || readString(session?.meshNodeFor)
|
|
550
|
+
);
|
|
551
|
+
if (hasMeshNodeFor) return false;
|
|
552
|
+
if (isMeshCoordinatorSessionRecord(session)) return false;
|
|
553
|
+
const launchedByCoordinator = Boolean(
|
|
554
|
+
session?.settings?.launchedByCoordinator === true || session?.meta?.launchedByCoordinator === true || session?.launchedByCoordinator === true
|
|
555
|
+
);
|
|
556
|
+
return !launchedByCoordinator;
|
|
557
|
+
}
|
|
558
|
+
function isWorkerTaskMode(taskMode) {
|
|
559
|
+
return taskMode !== "live_debug_readonly";
|
|
560
|
+
}
|
|
407
561
|
function addSessionRecord(target, session) {
|
|
408
562
|
if (!session || typeof session !== "object" || isTerminalSessionRecord(session)) return;
|
|
409
563
|
const sessionId = readSessionRecordId(session);
|
|
@@ -472,18 +626,26 @@ function queueAssignmentStaleReason(task, liveness) {
|
|
|
472
626
|
}
|
|
473
627
|
function buildQueueStatusSummary(queue) {
|
|
474
628
|
const counts = { pending: 0, assigned: 0, completed: 0, failed: 0, cancelled: 0 };
|
|
629
|
+
let staleAssigned = 0;
|
|
475
630
|
for (const task of queue) {
|
|
476
631
|
const status = typeof task?.status === "string" ? task.status : void 0;
|
|
477
632
|
if (status && Object.prototype.hasOwnProperty.call(counts, status)) {
|
|
478
633
|
counts[status] += 1;
|
|
479
634
|
}
|
|
635
|
+
if (status === "assigned" && task?.staleAssigned === true) staleAssigned += 1;
|
|
480
636
|
}
|
|
637
|
+
const liveAssigned = Math.max(0, counts.assigned - staleAssigned);
|
|
481
638
|
return {
|
|
482
639
|
totalCount: queue.length,
|
|
483
|
-
activeCount: counts.pending +
|
|
640
|
+
activeCount: counts.pending + liveAssigned,
|
|
484
641
|
historicalCount: counts.completed + counts.failed + counts.cancelled,
|
|
485
642
|
counts,
|
|
486
643
|
activeCounts: {
|
|
644
|
+
pending: counts.pending,
|
|
645
|
+
assigned: liveAssigned
|
|
646
|
+
},
|
|
647
|
+
staleAssignedCount: staleAssigned,
|
|
648
|
+
rawActiveCounts: {
|
|
487
649
|
pending: counts.pending,
|
|
488
650
|
assigned: counts.assigned
|
|
489
651
|
},
|
|
@@ -511,6 +673,18 @@ function filterQueueForView(queue, view, statuses) {
|
|
|
511
673
|
if (view === "historical") return queue.filter((task) => HISTORICAL_QUEUE_STATUSES.has(String(task?.status || "")));
|
|
512
674
|
return queue;
|
|
513
675
|
}
|
|
676
|
+
function prioritizeActiveQueueRows(queue) {
|
|
677
|
+
const active = [];
|
|
678
|
+
const historical = [];
|
|
679
|
+
const other = [];
|
|
680
|
+
for (const task of queue) {
|
|
681
|
+
const status = String(task?.status || "");
|
|
682
|
+
if (ACTIVE_QUEUE_STATUSES.has(status)) active.push(task);
|
|
683
|
+
else if (HISTORICAL_QUEUE_STATUSES.has(status)) historical.push(task);
|
|
684
|
+
else other.push(task);
|
|
685
|
+
}
|
|
686
|
+
return [...active, ...other, ...historical];
|
|
687
|
+
}
|
|
514
688
|
function slimQueueTask(task) {
|
|
515
689
|
return {
|
|
516
690
|
id: task?.id,
|
|
@@ -604,6 +778,172 @@ function unwrapCommandPayload(value) {
|
|
|
604
778
|
}
|
|
605
779
|
return current;
|
|
606
780
|
}
|
|
781
|
+
function isDirectDispatchLedgerEntry(entry) {
|
|
782
|
+
if (entry?.kind !== "task_dispatched") return false;
|
|
783
|
+
const payload = entry.payload || {};
|
|
784
|
+
const via = readString(payload.via);
|
|
785
|
+
return payload.source === "direct" || via === "p2p_direct" || via === "local_direct" || via === "mesh_send_task";
|
|
786
|
+
}
|
|
787
|
+
function readMessageTimestampIso(message) {
|
|
788
|
+
for (const value of [message?.timestamp, message?.createdAt, message?.created_at, message?.updatedAt, message?.time]) {
|
|
789
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
790
|
+
const ms = value > 1e10 ? value : value * 1e3;
|
|
791
|
+
return new Date(ms).toISOString();
|
|
792
|
+
}
|
|
793
|
+
if (typeof value === "string" && value.trim()) {
|
|
794
|
+
const ms = new Date(value.trim()).getTime();
|
|
795
|
+
if (Number.isFinite(ms)) return new Date(ms).toISOString();
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return void 0;
|
|
799
|
+
}
|
|
800
|
+
function readFinalAssistantTranscriptEvidence(payload) {
|
|
801
|
+
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
802
|
+
const finalAssistant = [...rawMessages].reverse().filter(isCoordinatorVisibleMessage).find((message) => {
|
|
803
|
+
const role = String(message?.role ?? "").toLowerCase();
|
|
804
|
+
return (role === "assistant" || role === "agent") && messageContent(message).trim();
|
|
805
|
+
});
|
|
806
|
+
const finalSummary = messageContent(finalAssistant).trim() || (typeof payload?.summary === "string" && payload.summary.trim() ? payload.summary.trim() : void 0);
|
|
807
|
+
return {
|
|
808
|
+
finalSummary,
|
|
809
|
+
transcriptMessageAt: finalAssistant ? readMessageTimestampIso(finalAssistant) : void 0
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function findNodeSession(nodes, nodeId, sessionId) {
|
|
813
|
+
if (!nodeId || !sessionId) return {};
|
|
814
|
+
const node = nodes.find((candidate) => readString(candidate?.id) === nodeId || readString(candidate?.nodeId) === nodeId);
|
|
815
|
+
if (!node) return {};
|
|
816
|
+
const sessions = Array.isArray(node.sessions) ? node.sessions : [];
|
|
817
|
+
const session = sessions.find((candidate) => readSessionRecordId(candidate) === sessionId);
|
|
818
|
+
return { node, session };
|
|
819
|
+
}
|
|
820
|
+
function buildDirectDispatchReconciliationCandidates(directDispatches, ledgerEntries) {
|
|
821
|
+
const candidates = [];
|
|
822
|
+
const seenTaskIds = /* @__PURE__ */ new Set();
|
|
823
|
+
for (const dispatch of directDispatches || []) {
|
|
824
|
+
const taskId = readString(dispatch?.taskId);
|
|
825
|
+
if (!taskId || seenTaskIds.has(taskId)) continue;
|
|
826
|
+
seenTaskIds.add(taskId);
|
|
827
|
+
candidates.push(dispatch);
|
|
828
|
+
}
|
|
829
|
+
for (const entry of ledgerEntries || []) {
|
|
830
|
+
if (!isDirectDispatchLedgerEntry(entry)) continue;
|
|
831
|
+
const taskId = readString(entry.payload?.taskId);
|
|
832
|
+
if (!taskId || seenTaskIds.has(taskId)) continue;
|
|
833
|
+
seenTaskIds.add(taskId);
|
|
834
|
+
candidates.push({
|
|
835
|
+
taskId,
|
|
836
|
+
nodeId: entry.nodeId,
|
|
837
|
+
sessionId: entry.sessionId,
|
|
838
|
+
providerType: entry.providerType || readString(entry.payload?.providerType),
|
|
839
|
+
message: readString(entry.payload?.message),
|
|
840
|
+
dispatchedAt: entry.timestamp,
|
|
841
|
+
via: readString(entry.payload?.via)
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return candidates;
|
|
845
|
+
}
|
|
846
|
+
async function reconcileDirectDispatchesFromTranscriptEvidence(ctx, liveNodes, directDispatches, ledgerEntries) {
|
|
847
|
+
let attempted = 0;
|
|
848
|
+
let reconciled = 0;
|
|
849
|
+
let skipped = 0;
|
|
850
|
+
const candidates = buildDirectDispatchReconciliationCandidates(directDispatches, ledgerEntries);
|
|
851
|
+
for (const dispatch of candidates) {
|
|
852
|
+
const taskId = readString(dispatch?.taskId);
|
|
853
|
+
const nodeId = readString(dispatch?.nodeId);
|
|
854
|
+
const sessionId = readString(dispatch?.sessionId);
|
|
855
|
+
if (!taskId || !nodeId || !sessionId) {
|
|
856
|
+
skipped += 1;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const { session } = findNodeSession(liveNodes, nodeId, sessionId);
|
|
860
|
+
if (!session || !isIdleSessionRecord(session)) {
|
|
861
|
+
skipped += 1;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const node = await findOptionalNodeWithRefresh(ctx, nodeId).catch(() => null);
|
|
865
|
+
if (!node) {
|
|
866
|
+
skipped += 1;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const providerType = readString(dispatch?.providerType) || resolveSessionProviderType(session);
|
|
870
|
+
const providerSessionId = readString(session?.providerSessionId) || readString(session?.activeChat?.providerSessionId) || readString(session?.settings?.providerSessionId) || resolveMeshSessionProviderMetadata(ctx, nodeId, sessionId)?.providerSessionId;
|
|
871
|
+
attempted += 1;
|
|
872
|
+
try {
|
|
873
|
+
const readResult = await commandForNode(ctx, node, "read_chat", {
|
|
874
|
+
sessionId,
|
|
875
|
+
targetSessionId: sessionId,
|
|
876
|
+
workspace: node.workspace,
|
|
877
|
+
...providerType ? { agentType: providerType, providerType } : {},
|
|
878
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
879
|
+
tailLimit: 10
|
|
880
|
+
});
|
|
881
|
+
const payload = unwrapCommandPayload(readResult);
|
|
882
|
+
if (payload?.success === false) continue;
|
|
883
|
+
const evidence = readFinalAssistantTranscriptEvidence(payload);
|
|
884
|
+
if (!evidence.finalSummary) continue;
|
|
885
|
+
const result = (0, import_daemon_core.reconcileDirectDispatchCompletionFromTranscript)({
|
|
886
|
+
meshId: ctx.mesh.id,
|
|
887
|
+
nodeId,
|
|
888
|
+
sessionId,
|
|
889
|
+
providerType,
|
|
890
|
+
providerSessionId: readString(payload?.providerSessionId) || providerSessionId,
|
|
891
|
+
taskId,
|
|
892
|
+
finalSummary: evidence.finalSummary,
|
|
893
|
+
transcriptMessageAt: evidence.transcriptMessageAt,
|
|
894
|
+
targetCoordinatorDaemonId: ctx.localDaemonId,
|
|
895
|
+
source: "mcp_mesh_status_transcript_reconciliation"
|
|
896
|
+
});
|
|
897
|
+
if (result.reconciled) reconciled += 1;
|
|
898
|
+
} catch {
|
|
899
|
+
skipped += 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return { attempted, reconciled, skipped };
|
|
903
|
+
}
|
|
904
|
+
async function triggerMeshQueueAndReport(ctx, node, opts) {
|
|
905
|
+
if (!(isLocalTransport(ctx.transport) || ctx.transport instanceof IpcTransport)) return void 0;
|
|
906
|
+
try {
|
|
907
|
+
let raw;
|
|
908
|
+
if (ctx.transport instanceof IpcTransport && node?.daemonId && opts?.localNode === false) {
|
|
909
|
+
raw = await ctx.transport.meshCommand(node.daemonId, "trigger_mesh_queue", { meshId: ctx.mesh.id });
|
|
910
|
+
} else if (isLocalTransport(ctx.transport)) {
|
|
911
|
+
raw = await ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id });
|
|
912
|
+
} else {
|
|
913
|
+
return void 0;
|
|
914
|
+
}
|
|
915
|
+
const payload = unwrapCommandPayload(raw);
|
|
916
|
+
const trigger = payload?.trigger && typeof payload.trigger === "object" ? payload.trigger : payload;
|
|
917
|
+
return trigger && typeof trigger === "object" ? trigger : { success: true };
|
|
918
|
+
} catch (e) {
|
|
919
|
+
return {
|
|
920
|
+
success: false,
|
|
921
|
+
error: e?.message || String(e)
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function buildQueueTriggerGuidance(queueTrigger) {
|
|
926
|
+
if (!queueTrigger || queueTrigger.claimed === true) return void 0;
|
|
927
|
+
if (queueTrigger.success === false) {
|
|
928
|
+
return {
|
|
929
|
+
queueClaimed: false,
|
|
930
|
+
queueDispatchState: "trigger_failed",
|
|
931
|
+
nextAction: "Do not assume the queued task is running. Check mesh_view_queue and daemon connectivity before redispatching."
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (queueTrigger.noIdleMeshSessionAvailable === true) {
|
|
935
|
+
return {
|
|
936
|
+
queueClaimed: false,
|
|
937
|
+
queueDispatchState: "pending_no_idle_mesh_session",
|
|
938
|
+
nextAction: "The task is queued but not running. Launch a managed worker with mesh_launch_session, or wait for a delegated session to become ready and trigger the queue again."
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
queueClaimed: false,
|
|
943
|
+
queueDispatchState: "pending_or_waiting_for_ready",
|
|
944
|
+
nextAction: "The task is queued but this trigger did not claim it. Use mesh_view_queue for the current active-work source of truth before retrying."
|
|
945
|
+
};
|
|
946
|
+
}
|
|
607
947
|
function isTerminalSessionRecord(session) {
|
|
608
948
|
const status = typeof session?.status === "string" ? session.status.toLowerCase() : "";
|
|
609
949
|
const lifecycle = typeof session?.lifecycle === "string" ? session.lifecycle.toLowerCase() : "";
|
|
@@ -616,22 +956,67 @@ function isIdleSessionRecord(session) {
|
|
|
616
956
|
const chatStatus = typeof session?.activeChat?.status === "string" ? session.activeChat.status.toLowerCase() : "";
|
|
617
957
|
return status === "idle" || chatStatus === "waiting_input";
|
|
618
958
|
}
|
|
959
|
+
function isMeshOwnedDelegateSession(session, meshId, nodeId) {
|
|
960
|
+
const settings = session?.settings;
|
|
961
|
+
const sessionMeshId = typeof settings?.meshNodeFor === "string" ? settings.meshNodeFor.trim() : "";
|
|
962
|
+
const sessionNodeId = typeof settings?.meshNodeId === "string" ? settings.meshNodeId.trim() : "";
|
|
963
|
+
if (sessionMeshId !== meshId) return false;
|
|
964
|
+
return !sessionNodeId || sessionNodeId === nodeId;
|
|
965
|
+
}
|
|
966
|
+
function hasRemoteRelayMetadata(session) {
|
|
967
|
+
return Boolean(
|
|
968
|
+
readString(session?.settings?.meshCoordinatorDaemonId) || readString(session?.meta?.meshCoordinatorDaemonId) || readString(session?.metadata?.meshCoordinatorDaemonId) || readString(session?.meshCoordinatorDaemonId)
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
function isRelaySafeRemoteDelegateSession(session, meshId, nodeId) {
|
|
972
|
+
return isMeshOwnedDelegateSession(session, meshId, nodeId) && hasRemoteRelayMetadata(session);
|
|
973
|
+
}
|
|
619
974
|
function chooseDispatchableSession(sessions, providerType, meshId, nodeId) {
|
|
620
975
|
const live = sessions.filter((session) => !isTerminalSessionRecord(session));
|
|
621
976
|
const matchingProvider = (session) => !providerType || session?.providerType === providerType || session?.cliType === providerType;
|
|
622
|
-
const isMeshOwnedDelegateSession = (session) => {
|
|
623
|
-
const settings = session?.settings;
|
|
624
|
-
const sessionMeshId = typeof settings?.meshNodeFor === "string" ? settings.meshNodeFor.trim() : "";
|
|
625
|
-
const coordinatorDaemonId = typeof settings?.meshCoordinatorDaemonId === "string" ? settings.meshCoordinatorDaemonId.trim() : "";
|
|
626
|
-
const sessionNodeId = typeof settings?.meshNodeId === "string" ? settings.meshNodeId.trim() : "";
|
|
627
|
-
if (sessionMeshId !== meshId || !coordinatorDaemonId) return false;
|
|
628
|
-
return !sessionNodeId || sessionNodeId === nodeId;
|
|
629
|
-
};
|
|
630
977
|
const meshSessions = live.filter(
|
|
631
|
-
(session) =>
|
|
978
|
+
(session) => isRelaySafeRemoteDelegateSession(session, meshId, nodeId)
|
|
632
979
|
);
|
|
633
980
|
return meshSessions.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || meshSessions.find(matchingProvider) || void 0;
|
|
634
981
|
}
|
|
982
|
+
function buildRelayUnsafeRemoteSessionFailure(ctx, node, sessionId, providerType) {
|
|
983
|
+
return {
|
|
984
|
+
success: false,
|
|
985
|
+
recoverable: true,
|
|
986
|
+
code: "mesh_delegate_session_missing_relay_metadata",
|
|
987
|
+
reason: "mesh_delegate_session_missing_relay_metadata",
|
|
988
|
+
transport: "mesh_transport",
|
|
989
|
+
retryRecommended: true,
|
|
990
|
+
meshId: ctx.mesh.id,
|
|
991
|
+
nodeId: node.id,
|
|
992
|
+
daemonId: node.daemonId,
|
|
993
|
+
workspace: node.workspace,
|
|
994
|
+
sessionId,
|
|
995
|
+
unsafeTranscriptAlias: true,
|
|
996
|
+
...providerType ? { resolvedProviderType: providerType } : {},
|
|
997
|
+
error: `Remote session '${sessionId}' is not relay-safe for mesh '${ctx.mesh.id}': missing meshNodeFor/meshCoordinatorDaemonId metadata, so completion events would not reach the coordinator ledger. This session may be the coordinator itself or an unrelated session (unsafe_transcript_alias risk).`,
|
|
998
|
+
nextAction: `Launch a fresh relay-safe session with mesh_launch_session(node_id: '${node.id}'${providerType ? `, type: '${providerType}'` : ""}) or dispatch without session_id so Repo Mesh can choose a valid delegate session.`,
|
|
999
|
+
noFallbackReason: "Blindly reusing a remote session without mesh relay metadata would silently drop task_completed / generating_completed events."
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function buildMissingCoordinatorDaemonIdFailure(ctx, node, providerType) {
|
|
1003
|
+
return {
|
|
1004
|
+
success: false,
|
|
1005
|
+
recoverable: true,
|
|
1006
|
+
code: "mesh_coordinator_daemon_unknown",
|
|
1007
|
+
reason: "mesh_coordinator_daemon_unknown",
|
|
1008
|
+
transport: "mesh_transport",
|
|
1009
|
+
retryRecommended: true,
|
|
1010
|
+
meshId: ctx.mesh.id,
|
|
1011
|
+
nodeId: node.id,
|
|
1012
|
+
daemonId: node.daemonId,
|
|
1013
|
+
workspace: node.workspace,
|
|
1014
|
+
...providerType ? { resolvedProviderType: providerType } : {},
|
|
1015
|
+
error: `Cannot launch a remote mesh delegate for node '${node.id}': coordinator daemon identity is unavailable, so the worker would be unable to relay completion events back to the coordinator.`,
|
|
1016
|
+
nextAction: "Retry after the coordinator daemon identity is available (for example from an attached daemon-backed MCP session) so meshCoordinatorDaemonId can be stamped on the worker session.",
|
|
1017
|
+
noFallbackReason: "Launching without meshCoordinatorDaemonId would create a worker session that can finish work but cannot emit task_completed / generating_completed back to the coordinator."
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
635
1020
|
function findNestedPayload(value, predicate) {
|
|
636
1021
|
const seen = /* @__PURE__ */ new Set();
|
|
637
1022
|
const stack = [{ payload: value, depth: 0 }];
|
|
@@ -659,12 +1044,16 @@ function extractGitDiff(value) {
|
|
|
659
1044
|
}
|
|
660
1045
|
function extractSubmodules(value, ignorePaths) {
|
|
661
1046
|
const payload = unwrapCommandPayload(value);
|
|
662
|
-
const subs = payload?.submodules ?? value?.submodules;
|
|
1047
|
+
const subs = payload?.status?.submodules ?? payload?.submodules ?? value?.status?.submodules ?? value?.submodules;
|
|
663
1048
|
if (!Array.isArray(subs)) return void 0;
|
|
664
1049
|
if (ignorePaths.length === 0) return subs;
|
|
665
1050
|
const ignoreSet = new Set(ignorePaths);
|
|
666
1051
|
return subs.filter((s) => s?.path && !ignoreSet.has(s.path));
|
|
667
1052
|
}
|
|
1053
|
+
function assignFullGitSnapshot(entry, status) {
|
|
1054
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) return;
|
|
1055
|
+
entry.git = status;
|
|
1056
|
+
}
|
|
668
1057
|
function extractLaunchPayload(value) {
|
|
669
1058
|
return findNestedPayload(value, (payload) => Boolean(payload?.sessionId || payload?.id || payload?.runtimeSessionId));
|
|
670
1059
|
}
|
|
@@ -789,20 +1178,76 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
789
1178
|
let sessionId = args.session_id?.trim() || "";
|
|
790
1179
|
const providerPriorityList = Array.isArray(node.policy?.providerPriority) ? node.policy.providerPriority : [];
|
|
791
1180
|
let resolvedProviderType = args.providerType?.trim() || providerPriorityList[0] || "";
|
|
792
|
-
if (
|
|
1181
|
+
if (sessionId && args.verifiedSession) {
|
|
1182
|
+
const explicitSession = args.verifiedSession;
|
|
1183
|
+
if (!isRelaySafeRemoteDelegateSession(explicitSession, ctx.mesh.id, node.id)) {
|
|
1184
|
+
return buildRelayUnsafeRemoteSessionFailure(
|
|
1185
|
+
ctx,
|
|
1186
|
+
node,
|
|
1187
|
+
sessionId,
|
|
1188
|
+
resolvedProviderType || resolveSessionProviderType(explicitSession) || void 0
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
if (!resolvedProviderType) {
|
|
1192
|
+
resolvedProviderType = resolveSessionProviderType(explicitSession);
|
|
1193
|
+
}
|
|
1194
|
+
} else if (!sessionId || args.session_id) {
|
|
793
1195
|
try {
|
|
794
1196
|
const relayResult = await transport.meshCommand(daemonId, "get_status_metadata", {});
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1197
|
+
const sessions = extractStatusMetadataSessions(relayResult);
|
|
1198
|
+
if (sessionId) {
|
|
1199
|
+
const explicitSession = sessions.find((session) => readSessionRecordId(session) === sessionId);
|
|
1200
|
+
if (!explicitSession) {
|
|
1201
|
+
return {
|
|
1202
|
+
success: false,
|
|
1203
|
+
recoverable: true,
|
|
1204
|
+
code: "mesh_target_session_not_found",
|
|
1205
|
+
reason: "mesh_target_session_not_found",
|
|
1206
|
+
transport: "mesh_transport",
|
|
1207
|
+
retryRecommended: true,
|
|
1208
|
+
meshId: ctx.mesh.id,
|
|
1209
|
+
nodeId: node.id,
|
|
1210
|
+
daemonId,
|
|
1211
|
+
workspace: node.workspace,
|
|
1212
|
+
sessionId,
|
|
1213
|
+
...resolvedProviderType ? { resolvedProviderType } : {},
|
|
1214
|
+
error: `Remote session '${sessionId}' is not present in the live status for node '${node.id}'.`,
|
|
1215
|
+
nextAction: `Launch a fresh session with mesh_launch_session(node_id: '${node.id}'${resolvedProviderType ? `, type: '${resolvedProviderType}'` : ""}) or retry without session_id so Repo Mesh can target a live delegate session.`
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
if (!isRelaySafeRemoteDelegateSession(explicitSession, ctx.mesh.id, node.id)) {
|
|
1219
|
+
return buildRelayUnsafeRemoteSessionFailure(
|
|
1220
|
+
ctx,
|
|
1221
|
+
node,
|
|
1222
|
+
sessionId,
|
|
1223
|
+
resolvedProviderType || resolveSessionProviderType(explicitSession) || void 0
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
801
1226
|
if (!resolvedProviderType) {
|
|
802
|
-
resolvedProviderType =
|
|
1227
|
+
resolvedProviderType = resolveSessionProviderType(explicitSession);
|
|
1228
|
+
}
|
|
1229
|
+
} else {
|
|
1230
|
+
const targetSession = chooseDispatchableSession(sessions, resolvedProviderType, ctx.mesh.id, node.id);
|
|
1231
|
+
if (targetSession?.id || targetSession?.sessionId) {
|
|
1232
|
+
sessionId = targetSession.id || targetSession.sessionId;
|
|
1233
|
+
if (!resolvedProviderType) {
|
|
1234
|
+
resolvedProviderType = resolveSessionProviderType(targetSession);
|
|
1235
|
+
}
|
|
803
1236
|
}
|
|
804
1237
|
}
|
|
805
1238
|
} catch (e) {
|
|
1239
|
+
if (sessionId) {
|
|
1240
|
+
return {
|
|
1241
|
+
...buildCoordinatorP2pRelayFailure(e, {
|
|
1242
|
+
command: "get_status_metadata",
|
|
1243
|
+
targetDaemonId: daemonId,
|
|
1244
|
+
nodeId: node.id,
|
|
1245
|
+
sessionId
|
|
1246
|
+
}),
|
|
1247
|
+
success: false,
|
|
1248
|
+
error: `Cannot verify remote session '${sessionId}' before dispatch: ${e?.message || String(e)}`
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
806
1251
|
}
|
|
807
1252
|
}
|
|
808
1253
|
if (!resolvedProviderType) {
|
|
@@ -814,7 +1259,8 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
814
1259
|
agentType: resolvedProviderType,
|
|
815
1260
|
cliType: resolvedProviderType,
|
|
816
1261
|
action: "send_chat",
|
|
817
|
-
message: args.message
|
|
1262
|
+
message: args.message,
|
|
1263
|
+
...args.meshContext ? { meshContext: args.meshContext } : {}
|
|
818
1264
|
});
|
|
819
1265
|
const dispatchPayload = unwrapCommandPayload(dispatchResult);
|
|
820
1266
|
if (dispatchPayload?.success === false || dispatchResult?.success === false) {
|
|
@@ -832,7 +1278,7 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
832
1278
|
error: `P2P dispatch failed: ${errorMessage}`
|
|
833
1279
|
};
|
|
834
1280
|
}
|
|
835
|
-
return { success: true, dispatched: true, sessionId: sessionId || resolvedProviderType };
|
|
1281
|
+
return { success: true, dispatched: true, sessionId: sessionId || resolvedProviderType, providerType: resolvedProviderType };
|
|
836
1282
|
} catch (e) {
|
|
837
1283
|
const errorMessage = e?.message || String(e);
|
|
838
1284
|
return {
|
|
@@ -862,34 +1308,197 @@ function resolveCoordinatorNode(ctx) {
|
|
|
862
1308
|
return void 0;
|
|
863
1309
|
}
|
|
864
1310
|
function readNodeMachineId(node) {
|
|
865
|
-
return readString(node.machineId) || readString(node.machine_id);
|
|
1311
|
+
return readString(node.machineId) || readString(node.machine_id) || readString(node.machine?.id) || readString(node.machine?.machineId) || readString(node.lastProbe?.machineId) || readString(node.last_probe?.machine_id) || readString(node.lastProbe?.machine?.id) || readString(node.lastProbe?.machine?.machineId) || readString(node.last_probe?.machine?.id) || readString(node.last_probe?.machine?.machine_id);
|
|
866
1312
|
}
|
|
867
1313
|
function readNodeDaemonId(node) {
|
|
868
|
-
return readString(node.daemonId) || readString(node.daemon_id);
|
|
1314
|
+
return readString(node.daemonId) || readString(node.daemon_id) || readString(node.machine?.daemonId) || readString(node.machine?.daemon_id) || readString(node.lastProbe?.daemonId) || readString(node.last_probe?.daemon_id) || readString(node.lastProbe?.machine?.daemonId) || readString(node.lastProbe?.machine?.daemon_id) || readString(node.last_probe?.machine?.daemonId) || readString(node.last_probe?.machine?.daemon_id);
|
|
1315
|
+
}
|
|
1316
|
+
function normalizeHostname(value) {
|
|
1317
|
+
const hostname = readString(value);
|
|
1318
|
+
if (!hostname) return void 0;
|
|
1319
|
+
return hostname.toLowerCase().replace(/\.$/, "");
|
|
1320
|
+
}
|
|
1321
|
+
function readNodeHostname(node) {
|
|
1322
|
+
return readString(node.hostname) || readString(node.host) || readString(node.machineHostname) || readString(node.machine_hostname) || readString(node.machine?.hostname) || readString(node.machine?.host) || readString(node.lastProbe?.hostname) || readString(node.last_probe?.hostname) || readString(node.lastProbe?.machine?.hostname) || readString(node.last_probe?.machine?.hostname);
|
|
1323
|
+
}
|
|
1324
|
+
function readNodeDisplayMachineName(node) {
|
|
1325
|
+
return readString(node.machineName) || readString(node.machine_name) || readString(node.machineLabel) || readString(node.machine_label) || readString(node.machineNickname) || readString(node.machine_nickname) || readString(node.alias) || readString(node.machine?.name) || readString(node.machine?.displayName) || readString(node.machine?.display_name) || readString(node.lastProbe?.machineName) || readString(node.last_probe?.machine_name) || readString(node.lastProbe?.machine?.name) || readString(node.last_probe?.machine?.name) || readNodeHostname(node);
|
|
1326
|
+
}
|
|
1327
|
+
function compactIdentityEvidence(value) {
|
|
1328
|
+
if (!value) return void 0;
|
|
1329
|
+
return value.length > 24 ? `${value.slice(0, 12)}\u2026${value.slice(-8)}` : value;
|
|
1330
|
+
}
|
|
1331
|
+
function pushIdentityEvidence(evidence, label, value) {
|
|
1332
|
+
const compact = compactIdentityEvidence(value);
|
|
1333
|
+
if (compact) evidence.push(`${label}:${compact}`);
|
|
1334
|
+
}
|
|
1335
|
+
function buildNodeMachineIdentity(ctx, node) {
|
|
1336
|
+
const machineId = readNodeMachineId(node);
|
|
1337
|
+
const daemonId = readNodeDaemonId(node);
|
|
1338
|
+
const hostname = readNodeHostname(node);
|
|
1339
|
+
const machineName = readNodeDisplayMachineName(node);
|
|
1340
|
+
const coordinatorHostname = readString(ctx.coordinatorHostname);
|
|
1341
|
+
const localControlPlaneReason = getLocalControlPlaneMatchReason(ctx, node);
|
|
1342
|
+
const directLocal = !!localControlPlaneReason;
|
|
1343
|
+
const hostnameMatches = Boolean(
|
|
1344
|
+
normalizeHostname(hostname) && normalizeHostname(coordinatorHostname) && normalizeHostname(hostname) === normalizeHostname(coordinatorHostname)
|
|
1345
|
+
);
|
|
1346
|
+
const sameMachine = directLocal || hostnameMatches;
|
|
1347
|
+
const evidence = [];
|
|
1348
|
+
pushIdentityEvidence(evidence, "machineName", machineName);
|
|
1349
|
+
pushIdentityEvidence(evidence, "hostname", hostname);
|
|
1350
|
+
pushIdentityEvidence(evidence, "machineId", machineId);
|
|
1351
|
+
pushIdentityEvidence(evidence, "daemonId", daemonId);
|
|
1352
|
+
if (localControlPlaneReason) {
|
|
1353
|
+
pushIdentityEvidence(evidence, "localMatch", localControlPlaneReason);
|
|
1354
|
+
pushIdentityEvidence(evidence, "localMachineId", ctx.localMachineId);
|
|
1355
|
+
pushIdentityEvidence(evidence, "localDaemonId", ctx.localDaemonId);
|
|
1356
|
+
}
|
|
1357
|
+
const locality = sameMachine ? "same_machine" : evidence.length > 0 ? "remote_known" : "remote_or_unknown";
|
|
1358
|
+
const localityReason = sameMachine ? localControlPlaneReason || "matched coordinator hostname" : evidence.length > 0 ? `known remote/other machine identity; no local coordinator match (${evidence.join(", ")})` : "no useful machine identity evidence available";
|
|
1359
|
+
return {
|
|
1360
|
+
daemonId,
|
|
1361
|
+
machineId,
|
|
1362
|
+
hostname,
|
|
1363
|
+
machineName,
|
|
1364
|
+
displayName: machineName || hostname || daemonId || machineId,
|
|
1365
|
+
coordinatorHostname,
|
|
1366
|
+
sameMachine,
|
|
1367
|
+
locality,
|
|
1368
|
+
localityReason,
|
|
1369
|
+
identityEvidence: evidence
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function nodeHasLocalDaemonEvidence(ctx, node) {
|
|
1373
|
+
const isLocal = (session) => {
|
|
1374
|
+
if (!session || typeof session !== "object") return false;
|
|
1375
|
+
if (ctx.localDaemonId && session.runtime?.owner === ctx.localDaemonId) return true;
|
|
1376
|
+
if (ctx.localDaemonId && session.daemonClient?.daemonId === ctx.localDaemonId) return true;
|
|
1377
|
+
return false;
|
|
1378
|
+
};
|
|
1379
|
+
const sessionArrays = [
|
|
1380
|
+
node?.sessions,
|
|
1381
|
+
node?.activeSessions,
|
|
1382
|
+
node?.active_sessions,
|
|
1383
|
+
node?.lastProbe?.sessions,
|
|
1384
|
+
node?.last_probe?.sessions,
|
|
1385
|
+
node?.lastProbe?.status?.sessions,
|
|
1386
|
+
node?.last_probe?.status?.sessions
|
|
1387
|
+
];
|
|
1388
|
+
for (const arr of sessionArrays) {
|
|
1389
|
+
if (Array.isArray(arr) && arr.some(isLocal)) return true;
|
|
1390
|
+
}
|
|
1391
|
+
const sessionRecords = [
|
|
1392
|
+
node?.activeSession,
|
|
1393
|
+
node?.active_session,
|
|
1394
|
+
node?.currentSession,
|
|
1395
|
+
node?.current_session,
|
|
1396
|
+
node?.runtimeSession,
|
|
1397
|
+
node?.runtime_session,
|
|
1398
|
+
node?.session,
|
|
1399
|
+
node?.lastProbe?.activeSession,
|
|
1400
|
+
node?.last_probe?.active_session,
|
|
1401
|
+
node?.lastProbe?.currentSession,
|
|
1402
|
+
node?.last_probe?.current_session,
|
|
1403
|
+
node?.lastProbe?.session,
|
|
1404
|
+
node?.last_probe?.session
|
|
1405
|
+
];
|
|
1406
|
+
for (const session of sessionRecords) {
|
|
1407
|
+
if (isLocal(session)) return true;
|
|
1408
|
+
}
|
|
1409
|
+
return false;
|
|
869
1410
|
}
|
|
870
1411
|
function isDirectLocalNode(ctx, node) {
|
|
871
1412
|
const machineId = readNodeMachineId(node);
|
|
872
1413
|
const daemonId = readNodeDaemonId(node);
|
|
873
1414
|
return Boolean(
|
|
874
|
-
ctx.localMachineId && machineId === ctx.localMachineId || ctx.localDaemonId && daemonId === ctx.localDaemonId
|
|
1415
|
+
ctx.localMachineId && machineId === ctx.localMachineId || ctx.localDaemonId && daemonId === ctx.localDaemonId || nodeHasLocalDaemonEvidence(ctx, node)
|
|
875
1416
|
);
|
|
876
1417
|
}
|
|
1418
|
+
function isConfiguredCoordinatorNode(ctx, node) {
|
|
1419
|
+
if (!ctx.localMachineId && !ctx.localDaemonId) return false;
|
|
1420
|
+
const nodeId = readString(node.id) || readString(node.nodeId) || readString(node.node_id);
|
|
1421
|
+
if (!nodeId) return false;
|
|
1422
|
+
const nodeDaemonId = readNodeDaemonId(node);
|
|
1423
|
+
const nodeMachineId = readNodeMachineId(node);
|
|
1424
|
+
if (nodeDaemonId && ctx.localDaemonId && nodeDaemonId !== ctx.localDaemonId) return false;
|
|
1425
|
+
if (nodeMachineId && ctx.localMachineId && nodeMachineId !== ctx.localMachineId) return false;
|
|
1426
|
+
const preferredNodeId = readString(ctx.mesh.coordinator?.preferredNodeId) || readString(ctx.mesh.coordinator?.preferred_node_id);
|
|
1427
|
+
if (preferredNodeId) return nodeId === preferredNodeId;
|
|
1428
|
+
const first = ctx.mesh.nodes?.[0];
|
|
1429
|
+
const firstNodeId = readString(first?.id) || readString(first?.nodeId) || readString(first?.node_id);
|
|
1430
|
+
return !!firstNodeId && nodeId === firstNodeId;
|
|
1431
|
+
}
|
|
1432
|
+
function getLocalControlPlaneMatchReason(ctx, node) {
|
|
1433
|
+
if (isDirectLocalNode(ctx, node)) return "matched coordinator daemon or machine id";
|
|
1434
|
+
if (isConfiguredCoordinatorNode(ctx, node)) return "matched configured coordinator node";
|
|
1435
|
+
if (node.isLocalWorktree === true) {
|
|
1436
|
+
const sourceNode = findClonedFromNode(ctx, node);
|
|
1437
|
+
if (sourceNode && isDirectLocalNode(ctx, sourceNode)) return "matched local cloned-from node";
|
|
1438
|
+
if (sourceNode && isConfiguredCoordinatorNode(ctx, sourceNode)) return "matched configured coordinator source node";
|
|
1439
|
+
}
|
|
1440
|
+
return void 0;
|
|
1441
|
+
}
|
|
877
1442
|
function findClonedFromNode(ctx, node) {
|
|
878
1443
|
const clonedFromNodeId = readString(node.clonedFromNodeId) || readString(node.cloned_from_node_id);
|
|
879
1444
|
if (!clonedFromNodeId) return void 0;
|
|
880
1445
|
return ctx.mesh.nodes.find((n) => n.id === clonedFromNodeId || n.nodeId === clonedFromNodeId || n.node_id === clonedFromNodeId);
|
|
881
1446
|
}
|
|
882
1447
|
function isLocalControlPlaneNode(ctx, node) {
|
|
883
|
-
|
|
884
|
-
if (node.isLocalWorktree === true) {
|
|
885
|
-
const sourceNode = findClonedFromNode(ctx, node);
|
|
886
|
-
if (sourceNode && isDirectLocalNode(ctx, sourceNode)) return true;
|
|
887
|
-
}
|
|
888
|
-
return false;
|
|
1448
|
+
return !!getLocalControlPlaneMatchReason(ctx, node);
|
|
889
1449
|
}
|
|
890
1450
|
function meshSessionCacheKey(nodeId, runtimeSessionId) {
|
|
891
1451
|
return `${nodeId}:${runtimeSessionId}`;
|
|
892
1452
|
}
|
|
1453
|
+
function rememberMeshSessionProviderMetadata(nodeId, runtimeSessionId, metadata) {
|
|
1454
|
+
const keyNodeId = readString(nodeId);
|
|
1455
|
+
const keySessionId = readString(runtimeSessionId);
|
|
1456
|
+
if (!keyNodeId || !keySessionId) return;
|
|
1457
|
+
const providerType = readString(metadata.providerType);
|
|
1458
|
+
const providerSessionId = readString(metadata.providerSessionId);
|
|
1459
|
+
if (!providerType && !providerSessionId) return;
|
|
1460
|
+
const existing = getSessionMetadata(meshSessionCacheKey(keyNodeId, keySessionId)) || { providerType: "" };
|
|
1461
|
+
meshSessionProviderMetadata.set(meshSessionCacheKey(keyNodeId, keySessionId), {
|
|
1462
|
+
providerType: providerType || existing.providerType,
|
|
1463
|
+
providerSessionId: providerSessionId || existing.providerSessionId,
|
|
1464
|
+
expiresAt: Date.now() + SESSION_PROVIDER_METADATA_TTL_MS
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
function rememberMeshSessionProviderMetadataFromEvent(event) {
|
|
1468
|
+
const metadataEvent = event?.metadataEvent && typeof event.metadataEvent === "object" ? event.metadataEvent : event && typeof event === "object" ? event : {};
|
|
1469
|
+
const nodeId = readString(event?.nodeId) || readString(metadataEvent.nodeId) || readString(metadataEvent.meshNodeId);
|
|
1470
|
+
const sessionId = readString(metadataEvent.targetSessionId) || readString(metadataEvent.sessionId) || readString(metadataEvent.instanceId) || readString(event?.sessionId);
|
|
1471
|
+
rememberMeshSessionProviderMetadata(nodeId, sessionId, {
|
|
1472
|
+
providerType: readString(metadataEvent.providerType) || readString(event?.providerType) || "",
|
|
1473
|
+
providerSessionId: readString(metadataEvent.providerSessionId) || readString(event?.providerSessionId)
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
function resolveMeshSessionProviderMetadataFromLedger(ctx, nodeId, runtimeSessionId) {
|
|
1477
|
+
const entries = (0, import_daemon_core.readLedgerEntries)(ctx.mesh.id, { tail: 50 });
|
|
1478
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1479
|
+
const entry = entries[i];
|
|
1480
|
+
const payload = entry.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload) ? entry.payload : {};
|
|
1481
|
+
const entryNodeId = readString(entry.nodeId) || readString(payload.nodeId) || readString(payload.meshNodeId);
|
|
1482
|
+
if (entryNodeId && entryNodeId !== nodeId) continue;
|
|
1483
|
+
const entrySessionId = readString(entry.sessionId) || readString(payload.targetSessionId) || readString(payload.sessionId) || readString(payload.instanceId);
|
|
1484
|
+
if (entrySessionId !== runtimeSessionId) continue;
|
|
1485
|
+
const providerType = readString(entry.providerType) || readString(payload.providerType);
|
|
1486
|
+
const completionDiagnostic = payload.completionDiagnostic && typeof payload.completionDiagnostic === "object" && !Array.isArray(payload.completionDiagnostic) ? payload.completionDiagnostic : {};
|
|
1487
|
+
const metadataEvent = payload.metadataEvent && typeof payload.metadataEvent === "object" && !Array.isArray(payload.metadataEvent) ? payload.metadataEvent : {};
|
|
1488
|
+
const providerSessionId = readString(payload.providerSessionId) || readString(completionDiagnostic.providerSessionId) || readString(metadataEvent.providerSessionId);
|
|
1489
|
+
if (providerType || providerSessionId) {
|
|
1490
|
+
return { providerType: providerType || "", providerSessionId };
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return void 0;
|
|
1494
|
+
}
|
|
1495
|
+
function resolveMeshSessionProviderMetadata(ctx, nodeId, runtimeSessionId) {
|
|
1496
|
+
const cached = getSessionMetadata(meshSessionCacheKey(nodeId, runtimeSessionId));
|
|
1497
|
+
if (cached?.providerType || cached?.providerSessionId) return cached;
|
|
1498
|
+
const fromLedger = resolveMeshSessionProviderMetadataFromLedger(ctx, nodeId, runtimeSessionId);
|
|
1499
|
+
if (fromLedger) rememberMeshSessionProviderMetadata(nodeId, runtimeSessionId, fromLedger);
|
|
1500
|
+
return fromLedger;
|
|
1501
|
+
}
|
|
893
1502
|
function countUncommittedChanges(status) {
|
|
894
1503
|
if (typeof status?.uncommittedChanges === "number") return status.uncommittedChanges;
|
|
895
1504
|
const keys = ["staged", "modified", "untracked", "deleted", "renamed"];
|
|
@@ -900,8 +1509,23 @@ function countUncommittedChanges(status) {
|
|
|
900
1509
|
function isGitStatusDirty(status) {
|
|
901
1510
|
if (typeof status?.isDirty === "boolean") return status.isDirty;
|
|
902
1511
|
if (typeof status?.dirty === "boolean") return status.dirty;
|
|
1512
|
+
if (Array.isArray(status?.submodules) && status.submodules.some((submodule) => submodule?.dirty || submodule?.outOfSync || submodule?.error)) return true;
|
|
903
1513
|
return countUncommittedChanges(status) > 0;
|
|
904
1514
|
}
|
|
1515
|
+
function slimLedgerPayload(payload) {
|
|
1516
|
+
const slim = {};
|
|
1517
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
1518
|
+
if (k === "message" || k === "taskSummary") {
|
|
1519
|
+
slim[k] = typeof v === "string" && v.length > 200 ? v.slice(0, 200) + "\u2026" : v;
|
|
1520
|
+
} else if (k === "evidence" || k === "workerResult" || k === "gitStatus" || k === "validationResults") {
|
|
1521
|
+
} else if (k === "finalSummary") {
|
|
1522
|
+
slim[k] = typeof v === "string" && v.length > 300 ? v.slice(0, 300) + "\u2026" : v;
|
|
1523
|
+
} else {
|
|
1524
|
+
slim[k] = v;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return slim;
|
|
1528
|
+
}
|
|
905
1529
|
function readRelatedRepos(node) {
|
|
906
1530
|
const raw = Array.isArray(node.relatedRepos) ? node.relatedRepos : Array.isArray(node.policy?.relatedRepos) ? node.policy.relatedRepos : [];
|
|
907
1531
|
return raw.map((entry) => ({
|
|
@@ -960,6 +1584,16 @@ function missingProviderPriorityMessage(nodeId) {
|
|
|
960
1584
|
return `Node '${nodeId}' has no providerPriority policy; pass type explicitly or configure node.policy.providerPriority`;
|
|
961
1585
|
}
|
|
962
1586
|
function getNodeLaunchReadiness(node) {
|
|
1587
|
+
const bootstrap = node.worktreeBootstrap;
|
|
1588
|
+
if (node.isLocalWorktree && bootstrap?.status === "failed" && bootstrap?.required !== false) {
|
|
1589
|
+
return {
|
|
1590
|
+
providerPriority: readProviderPriority(node.policy),
|
|
1591
|
+
launchReady: false,
|
|
1592
|
+
launchBlockedReason: "worktree_bootstrap_failed",
|
|
1593
|
+
launchBlockedMessage: typeof bootstrap.error === "string" && bootstrap.error.trim() ? bootstrap.error.trim() : "Required worktree bootstrap failed; resolve it before launching an agent into this node.",
|
|
1594
|
+
worktreeBootstrap: bootstrap
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
963
1597
|
const providerPriority = readProviderPriority(node.policy);
|
|
964
1598
|
if (providerPriority.length) {
|
|
965
1599
|
return {
|
|
@@ -974,6 +1608,33 @@ function getNodeLaunchReadiness(node) {
|
|
|
974
1608
|
launchBlockedMessage: missingProviderPriorityMessage(node.id)
|
|
975
1609
|
};
|
|
976
1610
|
}
|
|
1611
|
+
function getWorktreeBootstrapLaunchBlock(node) {
|
|
1612
|
+
const bootstrap = node.worktreeBootstrap;
|
|
1613
|
+
if (!node.isLocalWorktree || bootstrap?.status !== "failed" || bootstrap?.required === false) return void 0;
|
|
1614
|
+
return {
|
|
1615
|
+
success: false,
|
|
1616
|
+
code: "worktree_bootstrap_failed",
|
|
1617
|
+
error: typeof bootstrap.error === "string" && bootstrap.error.trim() ? bootstrap.error.trim() : `Node '${node.id}' has a failed required worktree bootstrap.`,
|
|
1618
|
+
nodeId: node.id,
|
|
1619
|
+
worktreeBootstrap: bootstrap,
|
|
1620
|
+
recoveryHint: "Fix the configured worktree bootstrap command or remove/recreate the worktree node before launching an agent."
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
async function collectLiveStatusSessions(ctx, node) {
|
|
1624
|
+
try {
|
|
1625
|
+
const statusResult = await commandForNode(ctx, node, "get_status_metadata", {});
|
|
1626
|
+
return extractStatusMetadataSessions(statusResult);
|
|
1627
|
+
} catch {
|
|
1628
|
+
return [];
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
async function collectMeshViewQueueNodesWithLiveSessions(ctx) {
|
|
1632
|
+
const nodes = await Promise.all(ctx.mesh.nodes.map(async (node) => {
|
|
1633
|
+
const liveSessions = await collectLiveStatusSessions(ctx, node);
|
|
1634
|
+
return liveSessions.length > 0 ? { ...node, sessions: liveSessions } : node;
|
|
1635
|
+
}));
|
|
1636
|
+
return nodes;
|
|
1637
|
+
}
|
|
977
1638
|
function readNumeric(value, fallback = 0) {
|
|
978
1639
|
const parsed = Number(value);
|
|
979
1640
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -1109,7 +1770,8 @@ async function commandForNode(ctx, node, command, args = {}) {
|
|
|
1109
1770
|
if (isLocalTransport(ctx.transport)) {
|
|
1110
1771
|
return ctx.transport.command(command, args);
|
|
1111
1772
|
}
|
|
1112
|
-
|
|
1773
|
+
const identity = buildNodeMachineIdentity(ctx, node);
|
|
1774
|
+
throw new Error(`Command '${command}' requires daemon IPC/local transport for node '${node.id}' (hostname=${identity.hostname || "unknown"}, coordinatorHostname=${identity.coordinatorHostname || "unknown"}, sameMachine=${identity.sameMachine})`);
|
|
1113
1775
|
}
|
|
1114
1776
|
function normalizePendingMeshCoordinatorEvents(value) {
|
|
1115
1777
|
const payload = unwrapCommandPayload(value);
|
|
@@ -1127,6 +1789,14 @@ function buildMeshForwardPayloadFromPendingEvent(event) {
|
|
|
1127
1789
|
providerType: readString(metadataEvent.providerType),
|
|
1128
1790
|
providerSessionId: readString(metadataEvent.providerSessionId),
|
|
1129
1791
|
finalSummary: readString(metadataEvent.finalSummary) || readString(metadataEvent.summary),
|
|
1792
|
+
jobId: readString(metadataEvent.jobId),
|
|
1793
|
+
interactionId: readString(metadataEvent.interactionId),
|
|
1794
|
+
status: readString(metadataEvent.status),
|
|
1795
|
+
targetDaemonId: readString(metadataEvent.targetDaemonId),
|
|
1796
|
+
startedAt: readString(metadataEvent.startedAt),
|
|
1797
|
+
completedAt: readString(metadataEvent.completedAt),
|
|
1798
|
+
retryOfJobId: readString(metadataEvent.retryOfJobId),
|
|
1799
|
+
...metadataEvent.result && typeof metadataEvent.result === "object" && !Array.isArray(metadataEvent.result) ? { result: metadataEvent.result } : {},
|
|
1130
1800
|
...metadataEvent.intentional === true ? { intentional: true } : {},
|
|
1131
1801
|
...metadataEvent.intentionalStop === true ? { intentionalStop: true } : {},
|
|
1132
1802
|
...metadataEvent.operatorCleanup === true ? { operatorCleanup: true } : {},
|
|
@@ -1141,10 +1811,16 @@ async function drainCoordinatorPendingEvents(ctx, opts) {
|
|
|
1141
1811
|
const matchesCurrentMesh = (event) => readString(event?.meshId) === ctx.mesh.id;
|
|
1142
1812
|
if (ctx.transport instanceof IpcTransport) {
|
|
1143
1813
|
const surfacedEvents = [];
|
|
1814
|
+
const coordinatorDaemonId = readString(ctx.localDaemonId);
|
|
1815
|
+
const pendingEventArgs = {
|
|
1816
|
+
meshId: ctx.mesh.id,
|
|
1817
|
+
...coordinatorDaemonId ? { coordinatorDaemonId } : {}
|
|
1818
|
+
};
|
|
1144
1819
|
try {
|
|
1145
1820
|
surfacedEvents.push(
|
|
1146
|
-
...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events",
|
|
1821
|
+
...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", pendingEventArgs)).filter(matchesCurrentMesh)
|
|
1147
1822
|
);
|
|
1823
|
+
surfacedEvents.forEach(rememberMeshSessionProviderMetadataFromEvent);
|
|
1148
1824
|
} catch {
|
|
1149
1825
|
}
|
|
1150
1826
|
for (const node of ctx.mesh.nodes) {
|
|
@@ -1152,27 +1828,31 @@ async function drainCoordinatorPendingEvents(ctx, opts) {
|
|
|
1152
1828
|
if (requestedNodeIds && !requestedNodeIds.has(node.id)) continue;
|
|
1153
1829
|
try {
|
|
1154
1830
|
const remoteEvents = normalizePendingMeshCoordinatorEvents(
|
|
1155
|
-
await ctx.transport.meshCommand(node.daemonId, "get_pending_mesh_events",
|
|
1831
|
+
await ctx.transport.meshCommand(node.daemonId, "get_pending_mesh_events", pendingEventArgs)
|
|
1156
1832
|
).filter(matchesCurrentMesh);
|
|
1157
1833
|
if (remoteEvents.length === 0) continue;
|
|
1158
1834
|
for (const event of remoteEvents) {
|
|
1159
1835
|
const payload = buildMeshForwardPayloadFromPendingEvent(event);
|
|
1160
1836
|
if (!payload.event || !payload.meshId) continue;
|
|
1161
1837
|
await ctx.transport.command("mesh_forward_event", payload);
|
|
1838
|
+
rememberMeshSessionProviderMetadataFromEvent({ ...event, metadataEvent: payload });
|
|
1162
1839
|
}
|
|
1163
1840
|
} catch {
|
|
1164
1841
|
}
|
|
1165
1842
|
}
|
|
1166
1843
|
try {
|
|
1167
1844
|
surfacedEvents.push(
|
|
1168
|
-
...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events",
|
|
1845
|
+
...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", pendingEventArgs)).filter(matchesCurrentMesh)
|
|
1169
1846
|
);
|
|
1847
|
+
surfacedEvents.forEach(rememberMeshSessionProviderMetadataFromEvent);
|
|
1170
1848
|
} catch {
|
|
1171
1849
|
}
|
|
1172
1850
|
return surfacedEvents;
|
|
1173
1851
|
}
|
|
1174
1852
|
if (isLocalTransport(ctx.transport)) {
|
|
1175
|
-
|
|
1853
|
+
const events = (0, import_daemon_core.drainPendingMeshCoordinatorEvents)(ctx.mesh.id, ctx.localDaemonId).filter(matchesCurrentMesh);
|
|
1854
|
+
events.forEach(rememberMeshSessionProviderMetadataFromEvent);
|
|
1855
|
+
return events;
|
|
1176
1856
|
}
|
|
1177
1857
|
return [];
|
|
1178
1858
|
}
|
|
@@ -1189,11 +1869,12 @@ function buildRemoveNodeArgs(ctx, nodeId, sessionCleanupMode) {
|
|
|
1189
1869
|
}
|
|
1190
1870
|
var MESH_STATUS_TOOL = {
|
|
1191
1871
|
name: "mesh_status",
|
|
1192
|
-
description: "Get the current status of all nodes in the repo mesh \u2014 health, git state, active sessions, recovery hints, and recommended next steps. Use this to decide which node to send work to or how to recover from failures.",
|
|
1872
|
+
description: "Get the current status of all nodes in the repo mesh \u2014 health, git state, active sessions, recovery hints, and recommended next steps. Use this to decide which node to send work to or how to recover from failures. Do not repeatedly call this to wait for generating delegated work; wait for pendingCoordinatorEvents/completion events or an explicit user status request.",
|
|
1193
1873
|
inputSchema: {
|
|
1194
1874
|
type: "object",
|
|
1195
1875
|
properties: {
|
|
1196
|
-
_gemini_compat: { type: "string", description: "Dummy property for Gemini compatibility. Ignore this." }
|
|
1876
|
+
_gemini_compat: { type: "string", description: "Dummy property for Gemini compatibility. Ignore this." },
|
|
1877
|
+
includeStaleDirectWorkDetails: { type: "boolean", description: "Opt in to the full staleDirectWork array. Defaults false; normal status returns compact staleDirectWorkSummary only." }
|
|
1197
1878
|
}
|
|
1198
1879
|
}
|
|
1199
1880
|
};
|
|
@@ -1213,14 +1894,18 @@ var MESH_ENQUEUE_TASK_TOOL = {
|
|
|
1213
1894
|
inputSchema: {
|
|
1214
1895
|
type: "object",
|
|
1215
1896
|
properties: {
|
|
1216
|
-
message: { type: "string", description: "The task instruction for the agent." }
|
|
1897
|
+
message: { type: "string", description: "The task instruction for the agent." },
|
|
1898
|
+
task_mode: { type: "string", enum: ["code_change", "validation", "live_debug_readonly", "launch_app", "convergence"], description: "Optional task-mode contract. live_debug_readonly rejects obvious write/commit/push/deploy/destructive instructions before dispatch." },
|
|
1899
|
+
taskMode: { type: "string", enum: ["code_change", "validation", "live_debug_readonly", "launch_app", "convergence"], description: "CamelCase alias for task_mode." },
|
|
1900
|
+
requiredTags: { type: "array", items: { type: "string" }, description: "Optional capability tags that every eligible node must have, e.g. os=darwin, provider=codex-cli, gpu." },
|
|
1901
|
+
required_tags: { type: "array", items: { type: "string" }, description: "Snake_case alias for requiredTags." }
|
|
1217
1902
|
},
|
|
1218
1903
|
required: ["message"]
|
|
1219
1904
|
}
|
|
1220
1905
|
};
|
|
1221
1906
|
var MESH_VIEW_QUEUE_TOOL = {
|
|
1222
1907
|
name: "mesh_view_queue",
|
|
1223
|
-
description: "View the mesh work queue with source-of-truth active counts separated from historical completed/failed/cancelled records.",
|
|
1908
|
+
description: "View the mesh work queue with source-of-truth active counts separated from historical completed/failed/cancelled records. Do not repeatedly call this to wait for generating assigned work; wait for pendingCoordinatorEvents/completion events or an explicit user status request.",
|
|
1224
1909
|
inputSchema: {
|
|
1225
1910
|
type: "object",
|
|
1226
1911
|
properties: {
|
|
@@ -1273,7 +1958,9 @@ var MESH_SEND_TASK_TOOL = {
|
|
|
1273
1958
|
properties: {
|
|
1274
1959
|
node_id: { type: "string", description: "Target node ID (from mesh_list_nodes)." },
|
|
1275
1960
|
session_id: { type: "string", description: "Agent session ID on the target node." },
|
|
1276
|
-
message: { type: "string", description: "Natural-language task to send to the agent." }
|
|
1961
|
+
message: { type: "string", description: "Natural-language task to send to the agent." },
|
|
1962
|
+
task_mode: { type: "string", enum: ["code_change", "validation", "live_debug_readonly", "launch_app", "convergence"], description: "Optional task-mode contract. live_debug_readonly rejects obvious write/commit/push/deploy/destructive instructions before local or remote direct dispatch." },
|
|
1963
|
+
taskMode: { type: "string", enum: ["code_change", "validation", "live_debug_readonly", "launch_app", "convergence"], description: "CamelCase alias for task_mode." }
|
|
1277
1964
|
},
|
|
1278
1965
|
required: ["node_id", "session_id", "message"]
|
|
1279
1966
|
}
|
|
@@ -1331,6 +2018,21 @@ var MESH_GIT_STATUS_TOOL = {
|
|
|
1331
2018
|
required: ["node_id"]
|
|
1332
2019
|
}
|
|
1333
2020
|
};
|
|
2021
|
+
var MESH_FAST_FORWARD_NODE_TOOL = {
|
|
2022
|
+
name: "mesh_fast_forward_node",
|
|
2023
|
+
description: "Safely dry-run or execute an obvious direct fast-forward for a mesh node without launching an agent session. Defaults to dry-run; execution requires execute=true. Never pushes, rebases, resets, cleans, or checks out arbitrary revisions.",
|
|
2024
|
+
inputSchema: {
|
|
2025
|
+
type: "object",
|
|
2026
|
+
properties: {
|
|
2027
|
+
node_id: { type: "string", description: "Target node ID." },
|
|
2028
|
+
branch: { type: "string", description: "Optional guard: require the node's current branch to match this branch before planning/executing." },
|
|
2029
|
+
execute: { type: "boolean", description: "When true, apply the fast-forward if all safety gates pass. Defaults false/dry-run." },
|
|
2030
|
+
dry_run: { type: "boolean", description: "Preview only. Defaults true unless execute=true; dry_run=true overrides execute." },
|
|
2031
|
+
update_submodules: { type: "boolean", description: "When true, if the root fast-forward changes gitlinks, run only git submodule update --init --recursive and verify submodules clean." }
|
|
2032
|
+
},
|
|
2033
|
+
required: ["node_id"]
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
1334
2036
|
var MESH_CHECKPOINT_TOOL = {
|
|
1335
2037
|
name: "mesh_checkpoint",
|
|
1336
2038
|
description: "Create a git checkpoint (commit) on a mesh node workspace.",
|
|
@@ -1414,7 +2116,7 @@ var MESH_TASK_HISTORY_TOOL = {
|
|
|
1414
2116
|
type: "object",
|
|
1415
2117
|
properties: {
|
|
1416
2118
|
tail: { type: "number", description: "Number of recent entries to return (default: 20)." },
|
|
1417
|
-
kind: { type: "string", description: "Filter by entry kind: task_dispatched, task_completed, task_failed, task_stalled, session_launched, checkpoint_created, node_cloned, node_removed." }
|
|
2119
|
+
kind: { type: "string", description: "Filter by entry kind: task_dispatched, task_completed, task_failed, task_stalled, session_launched, checkpoint_created, node_cloned, node_removed, direct_fast_forward." }
|
|
1418
2120
|
}
|
|
1419
2121
|
}
|
|
1420
2122
|
};
|
|
@@ -1434,7 +2136,7 @@ var MESH_RECONCILE_LEDGER_TOOL = {
|
|
|
1434
2136
|
};
|
|
1435
2137
|
var MESH_REFINE_NODE_TOOL = {
|
|
1436
2138
|
name: "mesh_refine_node",
|
|
1437
|
-
description: "The Refinery:
|
|
2139
|
+
description: "The Refinery: Accept an async validation/merge/cleanup job for a completed worktree node. The immediate response includes async:true, status:'accepted', jobId, interactionId, target node, and startedAt; completion/failure evidence is delivered through pending mesh events and the mesh task ledger.",
|
|
1438
2140
|
inputSchema: {
|
|
1439
2141
|
type: "object",
|
|
1440
2142
|
properties: {
|
|
@@ -1443,6 +2145,43 @@ var MESH_REFINE_NODE_TOOL = {
|
|
|
1443
2145
|
required: ["node_id"]
|
|
1444
2146
|
}
|
|
1445
2147
|
};
|
|
2148
|
+
var MESH_REFINE_CONFIG_SCHEMA_TOOL = {
|
|
2149
|
+
name: "mesh_refine_config_schema",
|
|
2150
|
+
description: "Return the Repo Mesh Refinery config JSON schema and supported repo-local config locations. This is the validation source of truth; heuristic command detection is suggestions-only.",
|
|
2151
|
+
inputSchema: { type: "object", properties: {} }
|
|
2152
|
+
};
|
|
2153
|
+
var MESH_VALIDATE_REFINE_CONFIG_TOOL = {
|
|
2154
|
+
name: "mesh_validate_refine_config",
|
|
2155
|
+
description: "Validate the repo mesh/refine config for a node/workspace without running validation commands or merging.",
|
|
2156
|
+
inputSchema: {
|
|
2157
|
+
type: "object",
|
|
2158
|
+
properties: {
|
|
2159
|
+
node_id: { type: "string", description: "Optional node/workspace whose refine config should be loaded. Defaults to the first mesh node." },
|
|
2160
|
+
config: { type: "object", description: "Optional inline config object to validate instead of loading from the repo." }
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
var MESH_SUGGEST_REFINE_CONFIG_TOOL = {
|
|
2165
|
+
name: "mesh_suggest_refine_config",
|
|
2166
|
+
description: "Suggest a repo mesh/refine config scaffold from project context/package scripts. Suggestions are never executed until saved as explicit refine config.",
|
|
2167
|
+
inputSchema: {
|
|
2168
|
+
type: "object",
|
|
2169
|
+
properties: {
|
|
2170
|
+
node_id: { type: "string", description: "Optional node/workspace used for suggestions. Defaults to the first mesh node." }
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
var MESH_REFINE_PLAN_TOOL = {
|
|
2175
|
+
name: "mesh_refine_plan",
|
|
2176
|
+
description: "Dry-run Refinery plan for a worktree node: reports config source, validation commands, suggestions/unavailable reason, and merge/cleanup intent without executing validation or git merge.",
|
|
2177
|
+
inputSchema: {
|
|
2178
|
+
type: "object",
|
|
2179
|
+
properties: {
|
|
2180
|
+
node_id: { type: "string", description: "Node ID of the worktree node to plan." }
|
|
2181
|
+
},
|
|
2182
|
+
required: ["node_id"]
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
1446
2185
|
var ALL_MESH_TOOLS = [
|
|
1447
2186
|
MESH_STATUS_TOOL,
|
|
1448
2187
|
MESH_LIST_NODES_TOOL,
|
|
@@ -1455,24 +2194,31 @@ var ALL_MESH_TOOLS = [
|
|
|
1455
2194
|
MESH_READ_DEBUG_TOOL,
|
|
1456
2195
|
MESH_LAUNCH_SESSION_TOOL,
|
|
1457
2196
|
MESH_GIT_STATUS_TOOL,
|
|
2197
|
+
MESH_FAST_FORWARD_NODE_TOOL,
|
|
1458
2198
|
MESH_CHECKPOINT_TOOL,
|
|
1459
2199
|
MESH_APPROVE_TOOL,
|
|
1460
2200
|
MESH_CLONE_NODE_TOOL,
|
|
1461
2201
|
MESH_REMOVE_NODE_TOOL,
|
|
1462
2202
|
MESH_REFINE_NODE_TOOL,
|
|
2203
|
+
MESH_REFINE_CONFIG_SCHEMA_TOOL,
|
|
2204
|
+
MESH_VALIDATE_REFINE_CONFIG_TOOL,
|
|
2205
|
+
MESH_SUGGEST_REFINE_CONFIG_TOOL,
|
|
2206
|
+
MESH_REFINE_PLAN_TOOL,
|
|
1463
2207
|
MESH_CLEANUP_SESSIONS_TOOL,
|
|
1464
2208
|
MESH_TASK_HISTORY_TOOL,
|
|
1465
2209
|
MESH_RECONCILE_LEDGER_TOOL
|
|
1466
2210
|
];
|
|
1467
|
-
async function meshStatus(ctx) {
|
|
2211
|
+
async function meshStatus(ctx, args = {}) {
|
|
1468
2212
|
await refreshMeshFromDaemon(ctx);
|
|
1469
2213
|
const { mesh, transport } = ctx;
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
for (const node of mesh.nodes) {
|
|
2214
|
+
let ledgerSummary = (0, import_daemon_core.getLedgerSummary)(mesh.id);
|
|
2215
|
+
const results = await Promise.all(mesh.nodes.map(async (node) => {
|
|
1473
2216
|
const entry = {
|
|
1474
2217
|
nodeId: node.id,
|
|
1475
2218
|
workspace: node.workspace,
|
|
2219
|
+
machine: buildNodeMachineIdentity(ctx, node),
|
|
2220
|
+
daemonId: readNodeDaemonId(node),
|
|
2221
|
+
machineId: readNodeMachineId(node),
|
|
1476
2222
|
...getNodeLaunchReadiness(node)
|
|
1477
2223
|
};
|
|
1478
2224
|
try {
|
|
@@ -1482,6 +2228,7 @@ async function meshStatus(ctx) {
|
|
|
1482
2228
|
const uncommittedChanges = countUncommittedChanges(status);
|
|
1483
2229
|
const dirty = isGitStatusDirty(status);
|
|
1484
2230
|
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
2231
|
+
assignFullGitSnapshot(entry, status);
|
|
1485
2232
|
entry.branch = status?.branch;
|
|
1486
2233
|
entry.isDirty = dirty;
|
|
1487
2234
|
entry.uncommittedChanges = uncommittedChanges;
|
|
@@ -1503,6 +2250,7 @@ async function meshStatus(ctx) {
|
|
|
1503
2250
|
const uncommittedChanges = countUncommittedChanges(status);
|
|
1504
2251
|
const dirty = isGitStatusDirty(status);
|
|
1505
2252
|
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
2253
|
+
assignFullGitSnapshot(entry, status);
|
|
1506
2254
|
entry.branch = status?.branch;
|
|
1507
2255
|
entry.isDirty = dirty;
|
|
1508
2256
|
entry.uncommittedChanges = uncommittedChanges;
|
|
@@ -1538,7 +2286,7 @@ async function meshStatus(ctx) {
|
|
|
1538
2286
|
if (recoveryContext.consecutiveNodeFailures > 0) {
|
|
1539
2287
|
entry.recoveryHints = {
|
|
1540
2288
|
consecutiveFailures: recoveryContext.consecutiveNodeFailures,
|
|
1541
|
-
lastTaskMessage: recoveryContext.lastTaskMessage,
|
|
2289
|
+
lastTaskMessage: typeof recoveryContext.lastTaskMessage === "string" ? recoveryContext.lastTaskMessage.slice(0, 100) + (recoveryContext.lastTaskMessage.length > 100 ? "\u2026" : "") : recoveryContext.lastTaskMessage,
|
|
1542
2290
|
advice: recoveryContext.advice,
|
|
1543
2291
|
retryRecommended: recoveryContext.retryRecommended
|
|
1544
2292
|
};
|
|
@@ -1578,7 +2326,55 @@ async function meshStatus(ctx) {
|
|
|
1578
2326
|
}
|
|
1579
2327
|
const relatedRepos = await collectRelatedRepoStatuses(ctx, node);
|
|
1580
2328
|
if (relatedRepos.length) entry.relatedRepos = relatedRepos;
|
|
1581
|
-
|
|
2329
|
+
const liveSessions = await collectLiveStatusSessions(ctx, node);
|
|
2330
|
+
if (liveSessions.length > 0) {
|
|
2331
|
+
entry.sessions = liveSessions.map((s) => {
|
|
2332
|
+
const coordinatorMeshId = typeof s.coordinator?.meshId === "string" ? s.coordinator.meshId : void 0;
|
|
2333
|
+
const isSelfCoordinator = coordinatorMeshId === mesh.id;
|
|
2334
|
+
return {
|
|
2335
|
+
id: s.instanceId ?? s.id ?? s.sessionId,
|
|
2336
|
+
status: s.status ?? s.lifecycle ?? s.state,
|
|
2337
|
+
providerType: s.providerType ?? s.cliType ?? s.type,
|
|
2338
|
+
...s.activeChat?.status ? { chatStatus: s.activeChat.status } : {},
|
|
2339
|
+
...isSelfCoordinator ? { isSelfCoordinator: true, role: "coordinator" } : {}
|
|
2340
|
+
};
|
|
2341
|
+
}).filter((s) => s.id);
|
|
2342
|
+
}
|
|
2343
|
+
return entry;
|
|
2344
|
+
}));
|
|
2345
|
+
let ledgerEntries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail: 200 });
|
|
2346
|
+
let directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(mesh.id);
|
|
2347
|
+
const directReconciliation = await reconcileDirectDispatchesFromTranscriptEvidence(ctx, results, directDispatches, ledgerEntries);
|
|
2348
|
+
if (directReconciliation.reconciled > 0) {
|
|
2349
|
+
ledgerEntries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail: 200 });
|
|
2350
|
+
directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(mesh.id);
|
|
2351
|
+
ledgerSummary = (0, import_daemon_core.getLedgerSummary)(mesh.id);
|
|
2352
|
+
}
|
|
2353
|
+
const activeWorkEvidence = (0, import_daemon_core.buildMeshActiveWork)({
|
|
2354
|
+
meshId: mesh.id,
|
|
2355
|
+
queue: (0, import_daemon_core.getQueue)(mesh.id),
|
|
2356
|
+
ledgerEntries,
|
|
2357
|
+
directDispatches,
|
|
2358
|
+
nodes: results
|
|
2359
|
+
});
|
|
2360
|
+
const pollingGuidance = buildActiveWorkPollingGuidance(activeWorkEvidence.summary);
|
|
2361
|
+
const staleDirectWorkSummary = (0, import_daemon_core.buildCompactStaleDirectWorkSummary)(activeWorkEvidence.staleDirectWork, {
|
|
2362
|
+
note: activeWorkEvidence.staleDirectWorkNote,
|
|
2363
|
+
detailHint: "Full stale direct entries are omitted from mesh_status by default. Call mesh_status with includeStaleDirectWorkDetails=true or inspect mesh_task_history for ledger detail."
|
|
2364
|
+
});
|
|
2365
|
+
const coordinatorSessions = [];
|
|
2366
|
+
for (const nodeEntry of results) {
|
|
2367
|
+
const sessions = Array.isArray(nodeEntry.sessions) ? nodeEntry.sessions : [];
|
|
2368
|
+
for (const s of sessions) {
|
|
2369
|
+
if (s?.isSelfCoordinator === true && s.id) {
|
|
2370
|
+
coordinatorSessions.push({
|
|
2371
|
+
nodeId: nodeEntry.nodeId,
|
|
2372
|
+
sessionId: s.id,
|
|
2373
|
+
providerType: s.providerType,
|
|
2374
|
+
status: s.status
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
1582
2378
|
}
|
|
1583
2379
|
const response = {
|
|
1584
2380
|
meshId: mesh.id,
|
|
@@ -1589,10 +2385,26 @@ async function meshStatus(ctx) {
|
|
|
1589
2385
|
sourceOfTruth: {
|
|
1590
2386
|
membership: "coordinator_daemon_live_mesh",
|
|
1591
2387
|
currentStatus: "live_git_and_session_probes",
|
|
2388
|
+
activeWork: "mesh_queue_file_and_local_ledger",
|
|
1592
2389
|
historicalEvidenceOnly: ["recoveryHints", "ledgerSummary"]
|
|
1593
2390
|
},
|
|
1594
2391
|
nodes: results,
|
|
1595
|
-
|
|
2392
|
+
activeWork: activeWorkEvidence.activeWork,
|
|
2393
|
+
staleDirectWorkSummary,
|
|
2394
|
+
...args.includeStaleDirectWorkDetails === true ? { staleDirectWork: activeWorkEvidence.staleDirectWork } : {},
|
|
2395
|
+
// terminalDirectWork is historical (completed/failed direct dispatches) — opt-in only.
|
|
2396
|
+
...args.includeTerminalDirectWork === true ? { terminalDirectWork: activeWorkEvidence.terminalDirectWork } : {},
|
|
2397
|
+
activeWorkSummary: activeWorkEvidence.summary,
|
|
2398
|
+
...pollingGuidance ? { pollingGuidance } : {},
|
|
2399
|
+
branchConvergenceSummary: summarizeBranchConvergence(results),
|
|
2400
|
+
...coordinatorSessions.length > 0 ? {
|
|
2401
|
+
coordinatorSessions,
|
|
2402
|
+
selfIdentification: {
|
|
2403
|
+
meshId: mesh.id,
|
|
2404
|
+
coordinatorSessions,
|
|
2405
|
+
note: "Sessions listed here are coordinator sessions for this mesh. The calling coordinator IS one of these sessions \u2014 do not treat its own generating CLI session as a foreign delegated task. Per-session marker: sessions[].isSelfCoordinator === true."
|
|
2406
|
+
}
|
|
2407
|
+
} : {}
|
|
1596
2408
|
};
|
|
1597
2409
|
try {
|
|
1598
2410
|
response.ledgerSummary = ledgerSummary;
|
|
@@ -1600,6 +2412,14 @@ async function meshStatus(ctx) {
|
|
|
1600
2412
|
}
|
|
1601
2413
|
try {
|
|
1602
2414
|
const pendingEvents = await drainCoordinatorPendingEvents(ctx);
|
|
2415
|
+
const asyncRefineJobs = (0, import_daemon_core.buildMeshAsyncRefineJobs)({
|
|
2416
|
+
meshId: mesh.id,
|
|
2417
|
+
ledgerEntries,
|
|
2418
|
+
pendingEvents
|
|
2419
|
+
});
|
|
2420
|
+
if (asyncRefineJobs.length > 0) {
|
|
2421
|
+
response.asyncRefineJobs = asyncRefineJobs;
|
|
2422
|
+
}
|
|
1603
2423
|
if (pendingEvents.length > 0) {
|
|
1604
2424
|
response.pendingCoordinatorEvents = pendingEvents;
|
|
1605
2425
|
}
|
|
@@ -1609,12 +2429,21 @@ async function meshStatus(ctx) {
|
|
|
1609
2429
|
}
|
|
1610
2430
|
async function meshTaskHistory(ctx, args) {
|
|
1611
2431
|
const { mesh } = ctx;
|
|
1612
|
-
await drainCoordinatorPendingEvents(ctx);
|
|
2432
|
+
const pendingEvents = await drainCoordinatorPendingEvents(ctx);
|
|
1613
2433
|
const tail = typeof args.tail === "number" && args.tail > 0 ? args.tail : 20;
|
|
1614
2434
|
const kind = typeof args.kind === "string" && args.kind.trim() ? [args.kind.trim()] : void 0;
|
|
1615
|
-
const
|
|
2435
|
+
const rawEntries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail, kind });
|
|
2436
|
+
const entries = rawEntries.map((e) => ({
|
|
2437
|
+
...e,
|
|
2438
|
+
payload: e.payload ? slimLedgerPayload(e.payload) : e.payload
|
|
2439
|
+
}));
|
|
1616
2440
|
const summary = (0, import_daemon_core.getLedgerSummary)(mesh.id);
|
|
1617
|
-
return JSON.stringify({
|
|
2441
|
+
return JSON.stringify({
|
|
2442
|
+
meshId: mesh.id,
|
|
2443
|
+
entries,
|
|
2444
|
+
summary,
|
|
2445
|
+
...pendingEvents.length > 0 ? { pendingCoordinatorEvents: pendingEvents } : {}
|
|
2446
|
+
}, null, 2);
|
|
1618
2447
|
}
|
|
1619
2448
|
async function meshReconcileLedger(ctx, args) {
|
|
1620
2449
|
await refreshMeshFromDaemon(ctx);
|
|
@@ -1704,6 +2533,9 @@ async function meshListNodes(ctx) {
|
|
|
1704
2533
|
nodeId: n.id,
|
|
1705
2534
|
workspace: n.workspace,
|
|
1706
2535
|
repoRoot: n.repoRoot,
|
|
2536
|
+
daemonId: readNodeDaemonId(n),
|
|
2537
|
+
machineId: readNodeMachineId(n),
|
|
2538
|
+
machine: buildNodeMachineIdentity(ctx, n),
|
|
1707
2539
|
isLocalWorktree: n.isLocalWorktree,
|
|
1708
2540
|
policy: n.policy,
|
|
1709
2541
|
relatedRepos: readRelatedRepos(n),
|
|
@@ -1713,57 +2545,135 @@ async function meshListNodes(ctx) {
|
|
|
1713
2545
|
}, null, 2);
|
|
1714
2546
|
}
|
|
1715
2547
|
async function meshEnqueueTask(ctx, args) {
|
|
2548
|
+
const taskMode = readString(args.task_mode) || readString(args.taskMode);
|
|
2549
|
+
const requiredTags = (0, import_daemon_core.normalizeMeshCapabilityTags)(Array.isArray(args.requiredTags) ? args.requiredTags : args.required_tags);
|
|
1716
2550
|
try {
|
|
1717
|
-
const task = (0, import_daemon_core.enqueueTask)(ctx.mesh.id, args.message);
|
|
2551
|
+
const task = (0, import_daemon_core.enqueueTask)(ctx.mesh.id, args.message, { taskMode, requiredTags });
|
|
1718
2552
|
if (isLocalTransport(ctx.transport) && !(ctx.transport instanceof IpcTransport)) {
|
|
1719
|
-
|
|
2553
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx);
|
|
2554
|
+
return JSON.stringify({
|
|
2555
|
+
success: true,
|
|
2556
|
+
source: "queue",
|
|
2557
|
+
taskId: task.id,
|
|
2558
|
+
status: task.status,
|
|
2559
|
+
taskMode: task.taskMode,
|
|
2560
|
+
requiredTags: task.requiredTags,
|
|
2561
|
+
queueTrigger,
|
|
2562
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
1720
2563
|
});
|
|
1721
|
-
return JSON.stringify({ success: true, taskId: task.id, status: task.status });
|
|
1722
2564
|
}
|
|
1723
2565
|
if (ctx.transport instanceof IpcTransport) {
|
|
1724
|
-
|
|
1725
|
-
});
|
|
2566
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx);
|
|
1726
2567
|
const dispatchPromises = [];
|
|
1727
2568
|
for (const node of ctx.mesh.nodes) {
|
|
1728
2569
|
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
1729
2570
|
if (isLocalNode || !node.daemonId) continue;
|
|
2571
|
+
if (!(0, import_daemon_core.nodeSatisfiesRequiredTags)(requiredTags, (0, import_daemon_core.buildMeshNodeCapabilityTags)(node))) continue;
|
|
1730
2572
|
dispatchPromises.push(
|
|
1731
2573
|
ipcDispatchToRemoteAgent(ctx, node, { message: args.message }).then((result) => {
|
|
1732
2574
|
if (result.success) {
|
|
1733
2575
|
try {
|
|
2576
|
+
const providerType = result.providerType;
|
|
2577
|
+
const descriptor = summarizeTaskMessage(args.message);
|
|
1734
2578
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
1735
2579
|
kind: "task_dispatched",
|
|
1736
2580
|
nodeId: node.id,
|
|
1737
2581
|
sessionId: result.sessionId,
|
|
1738
|
-
|
|
2582
|
+
providerType,
|
|
2583
|
+
payload: {
|
|
2584
|
+
source: "queue",
|
|
2585
|
+
via: "p2p_direct",
|
|
2586
|
+
taskId: task.id,
|
|
2587
|
+
message: args.message,
|
|
2588
|
+
taskTitle: descriptor.taskTitle,
|
|
2589
|
+
taskSummary: descriptor.taskSummary,
|
|
2590
|
+
...task.taskMode ? { taskMode: task.taskMode } : {},
|
|
2591
|
+
...providerType ? { providerType } : {},
|
|
2592
|
+
targetSessionId: result.sessionId
|
|
2593
|
+
}
|
|
1739
2594
|
});
|
|
1740
2595
|
} catch {
|
|
1741
2596
|
}
|
|
1742
2597
|
}
|
|
1743
|
-
}).catch(() => {
|
|
2598
|
+
}).catch((err) => {
|
|
2599
|
+
try {
|
|
2600
|
+
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
2601
|
+
kind: "p2p_dispatch_failed",
|
|
2602
|
+
nodeId: node.id,
|
|
2603
|
+
payload: {
|
|
2604
|
+
source: "queue",
|
|
2605
|
+
via: "p2p_direct",
|
|
2606
|
+
taskId: task.id,
|
|
2607
|
+
error: err?.message || String(err),
|
|
2608
|
+
dispatchFailedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
} catch {
|
|
2612
|
+
}
|
|
1744
2613
|
})
|
|
1745
2614
|
);
|
|
1746
2615
|
}
|
|
1747
2616
|
Promise.all(dispatchPromises).catch(() => {
|
|
1748
2617
|
});
|
|
1749
|
-
return JSON.stringify({
|
|
2618
|
+
return JSON.stringify({
|
|
2619
|
+
success: true,
|
|
2620
|
+
source: "queue",
|
|
2621
|
+
taskId: task.id,
|
|
2622
|
+
status: task.status,
|
|
2623
|
+
taskMode: task.taskMode,
|
|
2624
|
+
requiredTags: task.requiredTags,
|
|
2625
|
+
queueTrigger,
|
|
2626
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
2627
|
+
});
|
|
1750
2628
|
}
|
|
1751
|
-
return JSON.stringify({ success: true, taskId: task.id, status: task.status });
|
|
2629
|
+
return JSON.stringify({ success: true, source: "queue", taskId: task.id, status: task.status, taskMode: task.taskMode, requiredTags: task.requiredTags });
|
|
1752
2630
|
} catch (e) {
|
|
1753
|
-
|
|
2631
|
+
const message = e?.message || String(e);
|
|
2632
|
+
if (message.includes("live_debug_readonly_guardrail_violation")) {
|
|
2633
|
+
return JSON.stringify({ success: false, code: "live_debug_readonly_guardrail_violation", taskMode, error: message });
|
|
2634
|
+
}
|
|
2635
|
+
return JSON.stringify({ success: false, error: message });
|
|
1754
2636
|
}
|
|
1755
2637
|
}
|
|
1756
2638
|
async function meshViewQueue(ctx, args) {
|
|
1757
2639
|
try {
|
|
2640
|
+
await refreshMeshFromDaemon(ctx);
|
|
1758
2641
|
const statusFilter = sanitizeQueueStatusFilter(args.status);
|
|
1759
2642
|
const view = normalizeQueueViewMode(args.view);
|
|
1760
|
-
const fullQueue = annotateQueueStaleness((0, import_daemon_core.getQueue)(ctx.mesh.id), ctx.mesh);
|
|
2643
|
+
const fullQueue = prioritizeActiveQueueRows(annotateQueueStaleness((0, import_daemon_core.getQueue)(ctx.mesh.id), ctx.mesh));
|
|
1761
2644
|
const queue = filterQueueForView(fullQueue, view, statusFilter);
|
|
1762
2645
|
const summary = buildQueueStatusSummary(fullQueue);
|
|
1763
2646
|
const visibleSummary = buildQueueStatusSummary(queue);
|
|
1764
2647
|
const maintenance = buildQueueMaintenanceReport(fullQueue);
|
|
2648
|
+
const liveNodes = await collectMeshViewQueueNodesWithLiveSessions(ctx);
|
|
2649
|
+
let ledgerEntries = (0, import_daemon_core.readLedgerEntries)(ctx.mesh.id, { tail: 200 });
|
|
2650
|
+
let directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(ctx.mesh.id);
|
|
2651
|
+
const directReconciliation = await reconcileDirectDispatchesFromTranscriptEvidence(ctx, liveNodes, directDispatches, ledgerEntries);
|
|
2652
|
+
if (directReconciliation.reconciled > 0) {
|
|
2653
|
+
ledgerEntries = (0, import_daemon_core.readLedgerEntries)(ctx.mesh.id, { tail: 200 });
|
|
2654
|
+
directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(ctx.mesh.id);
|
|
2655
|
+
}
|
|
2656
|
+
(0, import_daemon_core.markStaleDirectDispatches)(ctx.mesh.id);
|
|
2657
|
+
directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(ctx.mesh.id);
|
|
2658
|
+
const activeWorkEvidence = (0, import_daemon_core.buildMeshActiveWork)({
|
|
2659
|
+
meshId: ctx.mesh.id,
|
|
2660
|
+
queue: fullQueue,
|
|
2661
|
+
ledgerEntries,
|
|
2662
|
+
// Always pass BeadsDB records (may be empty). buildMeshActiveWork uses them for local
|
|
2663
|
+
// dispatches and falls through to ledger scan for remote P2P dispatches not in BeadsDB.
|
|
2664
|
+
directDispatches,
|
|
2665
|
+
nodes: liveNodes
|
|
2666
|
+
});
|
|
2667
|
+
const recentDispatchFailures = ledgerEntries.filter((e) => e.kind === "p2p_dispatch_failed").slice(-20).map((e) => ({
|
|
2668
|
+
nodeId: e.nodeId,
|
|
2669
|
+
taskId: e.payload?.taskId,
|
|
2670
|
+
error: e.payload?.error,
|
|
2671
|
+
via: e.payload?.via,
|
|
2672
|
+
failedAt: e.payload?.dispatchFailedAt || e.timestamp
|
|
2673
|
+
}));
|
|
1765
2674
|
const staleAssignedTasks = maintenance.staleAssignedTasks || [];
|
|
1766
2675
|
const requestedHistoricalRows = queue.some((task) => HISTORICAL_QUEUE_STATUSES.has(String(task?.status || "")));
|
|
2676
|
+
const pollingGuidance = buildActiveWorkPollingGuidance(activeWorkEvidence.summary);
|
|
1767
2677
|
return JSON.stringify({
|
|
1768
2678
|
success: true,
|
|
1769
2679
|
sourceOfTruth: {
|
|
@@ -1778,21 +2688,29 @@ async function meshViewQueue(ctx, args) {
|
|
|
1778
2688
|
filtered: Boolean(statusFilter?.length) || view !== "all"
|
|
1779
2689
|
},
|
|
1780
2690
|
queue,
|
|
1781
|
-
|
|
1782
|
-
|
|
2691
|
+
activeWork: activeWorkEvidence.activeWork,
|
|
2692
|
+
staleDirectWork: activeWorkEvidence.staleDirectWork,
|
|
2693
|
+
activeWorkSummary: activeWorkEvidence.summary,
|
|
2694
|
+
...pollingGuidance ? { pollingGuidance } : {},
|
|
1783
2695
|
summary,
|
|
2696
|
+
visibleSummary,
|
|
1784
2697
|
activeCounts: summary.activeCounts,
|
|
1785
2698
|
historicalCounts: summary.historicalCounts,
|
|
1786
|
-
activeCount: summary.activeCount,
|
|
1787
|
-
historicalCount: summary.historicalCount,
|
|
1788
2699
|
visibleActiveCounts: visibleSummary.activeCounts,
|
|
1789
2700
|
visibleHistoricalCounts: visibleSummary.historicalCounts,
|
|
2701
|
+
activeCount: summary.activeCount,
|
|
2702
|
+
historicalCount: summary.historicalCount,
|
|
1790
2703
|
visibleActiveCount: visibleSummary.activeCount,
|
|
1791
2704
|
visibleHistoricalCount: visibleSummary.historicalCount,
|
|
1792
2705
|
staleAssignedTasks,
|
|
1793
2706
|
staleAssignedCount: maintenance.staleAssignedCount,
|
|
1794
2707
|
queueMaintenance: maintenance,
|
|
1795
2708
|
cleanupDryRun: maintenance,
|
|
2709
|
+
...recentDispatchFailures.length > 0 ? {
|
|
2710
|
+
recentDispatchFailures,
|
|
2711
|
+
dispatchFailureCount: recentDispatchFailures.length,
|
|
2712
|
+
dispatchFailureNote: "Remote P2P dispatch attempts that failed. Affected tasks remain pending and may require mesh_queue_requeue if no idle session picks them up."
|
|
2713
|
+
} : {},
|
|
1796
2714
|
...view === "active" || statusFilter?.some((status) => ACTIVE_QUEUE_STATUSES.has(status)) ? {
|
|
1797
2715
|
activeQueue: queue.filter((task) => ACTIVE_QUEUE_STATUSES.has(String(task?.status || "")))
|
|
1798
2716
|
} : {},
|
|
@@ -1812,6 +2730,10 @@ async function meshQueueCancel(ctx, args) {
|
|
|
1812
2730
|
if (!taskId) return JSON.stringify({ success: false, error: "task_id required" });
|
|
1813
2731
|
const task = (0, import_daemon_core.cancelTask)(ctx.mesh.id, taskId, { reason: args.reason });
|
|
1814
2732
|
if (!task) return JSON.stringify({ success: false, error: `Queue task '${taskId}' not found` });
|
|
2733
|
+
if (isLocalTransport(ctx.transport)) {
|
|
2734
|
+
ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
1815
2737
|
return JSON.stringify({ success: true, task }, null, 2);
|
|
1816
2738
|
} catch (e) {
|
|
1817
2739
|
return JSON.stringify({ success: false, error: e.message });
|
|
@@ -1842,10 +2764,60 @@ async function meshQueueRequeue(ctx, args) {
|
|
|
1842
2764
|
}
|
|
1843
2765
|
}
|
|
1844
2766
|
async function meshSendTask(ctx, args) {
|
|
2767
|
+
const requestedTaskMode = readString(args.task_mode) || readString(args.taskMode);
|
|
2768
|
+
const modeValidation = (0, import_daemon_core.validateMeshTaskModeRequest)(requestedTaskMode, args.message);
|
|
2769
|
+
if (!modeValidation.valid) {
|
|
2770
|
+
return JSON.stringify({
|
|
2771
|
+
success: false,
|
|
2772
|
+
code: "live_debug_readonly_guardrail_violation",
|
|
2773
|
+
taskMode: modeValidation.taskMode || requestedTaskMode,
|
|
2774
|
+
violations: modeValidation.violations,
|
|
2775
|
+
allowedOperations: modeValidation.allowedOperations,
|
|
2776
|
+
error: `live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(", ")})`
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
const taskMode = modeValidation.taskMode;
|
|
1845
2780
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
1846
2781
|
if (node.policy?.readOnly) {
|
|
1847
2782
|
return JSON.stringify({ error: `Node '${args.node_id}' is read-only` });
|
|
1848
2783
|
}
|
|
2784
|
+
let explicitTargetSession;
|
|
2785
|
+
if (args.session_id && isWorkerTaskMode(taskMode) && (ctx.transport instanceof IpcTransport || isLocalTransport(ctx.transport))) {
|
|
2786
|
+
try {
|
|
2787
|
+
const statusResult = await commandForNode(ctx, node, "get_status_metadata", {});
|
|
2788
|
+
const sessions = extractStatusMetadataSessions(statusResult);
|
|
2789
|
+
explicitTargetSession = sessions.find((session) => readSessionRecordId(session) === args.session_id);
|
|
2790
|
+
if (explicitTargetSession && isMeshCoordinatorSessionRecord(explicitTargetSession)) {
|
|
2791
|
+
return JSON.stringify({
|
|
2792
|
+
success: false,
|
|
2793
|
+
recoverable: true,
|
|
2794
|
+
code: "mesh_target_session_is_coordinator",
|
|
2795
|
+
reason: "mesh_target_session_is_coordinator",
|
|
2796
|
+
nodeId: args.node_id,
|
|
2797
|
+
sessionId: args.session_id,
|
|
2798
|
+
taskMode: taskMode || "unspecified",
|
|
2799
|
+
error: `Session '${args.session_id}' is a Repo Mesh coordinator session, not a visible worker session. Launch or use a visible worker session before dispatching this task.`,
|
|
2800
|
+
nextAction: `Call mesh_launch_session for node '${args.node_id}' and then retry mesh_send_task with that worker session_id, or use mesh_enqueue_task for queue-based worker assignment.`
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
if (explicitTargetSession && isUnmanagedSessionRecord(explicitTargetSession)) {
|
|
2804
|
+
return JSON.stringify({
|
|
2805
|
+
success: false,
|
|
2806
|
+
recoverable: true,
|
|
2807
|
+
code: "mesh_target_session_unmanaged",
|
|
2808
|
+
reason: "mesh_target_session_unmanaged",
|
|
2809
|
+
nodeId: args.node_id,
|
|
2810
|
+
sessionId: args.session_id,
|
|
2811
|
+
taskMode: taskMode || "unspecified",
|
|
2812
|
+
unsafeTranscriptAlias: true,
|
|
2813
|
+
error: `Session '${args.session_id}' on node '${args.node_id}' has no Repo Mesh delegation metadata (missing meshNodeFor/meshCoordinatorFor/launchedByCoordinator). It may be the coordinator's own session or an unrelated session \u2014 dispatching risks self-send and orphaned completion events that never reach the coordinator ledger.`,
|
|
2814
|
+
nextAction: `Call mesh_launch_session for node '${args.node_id}' to start a fresh managed worker session, then retry mesh_send_task with the returned session_id. Alternatively use mesh_enqueue_task for queue-based assignment without specifying session_id.`
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
} catch {
|
|
2818
|
+
explicitTargetSession = void 0;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
1849
2821
|
const duplicate = hasRecentDuplicateDispatch(ctx, args);
|
|
1850
2822
|
if (duplicate.duplicate) {
|
|
1851
2823
|
return JSON.stringify({
|
|
@@ -1869,47 +2841,162 @@ async function meshSendTask(ctx, args) {
|
|
|
1869
2841
|
const res = await ctx.transport.meshEnqueueTask(node.daemonId, {
|
|
1870
2842
|
meshId: ctx.mesh.id,
|
|
1871
2843
|
message: args.message,
|
|
1872
|
-
targetNodeId: args.node_id
|
|
2844
|
+
targetNodeId: args.node_id,
|
|
2845
|
+
...taskMode ? { taskMode } : {}
|
|
1873
2846
|
});
|
|
1874
2847
|
return JSON.stringify(res);
|
|
1875
2848
|
}
|
|
1876
2849
|
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
1877
2850
|
if (ctx.transport instanceof IpcTransport && node.daemonId && !isLocalNode) {
|
|
1878
|
-
const cached =
|
|
2851
|
+
const cached = getSessionMetadata(meshSessionCacheKey(args.node_id, args.session_id || ""));
|
|
2852
|
+
const taskId = (0, import_node_crypto.randomUUID)();
|
|
1879
2853
|
const result2 = await ipcDispatchToRemoteAgent(ctx, node, {
|
|
1880
2854
|
session_id: args.session_id,
|
|
1881
2855
|
message: args.message,
|
|
1882
|
-
providerType: cached?.providerType
|
|
2856
|
+
providerType: cached?.providerType,
|
|
2857
|
+
verifiedSession: explicitTargetSession,
|
|
2858
|
+
meshContext: {
|
|
2859
|
+
meshId: ctx.mesh.id,
|
|
2860
|
+
nodeId: args.node_id,
|
|
2861
|
+
taskId
|
|
2862
|
+
}
|
|
1883
2863
|
});
|
|
1884
2864
|
if (result2.success) {
|
|
1885
2865
|
const dispatchedSessionId = args.session_id || result2.sessionId;
|
|
2866
|
+
const dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1886
2867
|
try {
|
|
2868
|
+
const providerType = result2.providerType || cached?.providerType;
|
|
1887
2869
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
1888
2870
|
kind: "task_dispatched",
|
|
1889
2871
|
nodeId: args.node_id,
|
|
1890
2872
|
sessionId: dispatchedSessionId,
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
2873
|
+
providerType,
|
|
2874
|
+
payload: buildDirectTaskPayload(args.message, "p2p_direct", {
|
|
2875
|
+
taskId,
|
|
2876
|
+
taskMode,
|
|
2877
|
+
providerType,
|
|
2878
|
+
targetSessionId: dispatchedSessionId
|
|
2879
|
+
})
|
|
2880
|
+
});
|
|
2881
|
+
(0, import_daemon_core.insertDirectDispatch)(ctx.mesh.id, {
|
|
2882
|
+
taskId,
|
|
2883
|
+
nodeId: args.node_id,
|
|
2884
|
+
sessionId: dispatchedSessionId,
|
|
2885
|
+
providerType: providerType || void 0,
|
|
2886
|
+
message: args.message,
|
|
2887
|
+
taskMode: taskMode || void 0,
|
|
2888
|
+
via: "p2p_direct",
|
|
2889
|
+
dispatchedAt
|
|
1896
2890
|
});
|
|
1897
2891
|
} catch {
|
|
1898
2892
|
}
|
|
1899
2893
|
}
|
|
1900
|
-
return JSON.stringify({
|
|
2894
|
+
return JSON.stringify({
|
|
2895
|
+
...result2,
|
|
2896
|
+
nodeId: args.node_id,
|
|
2897
|
+
sessionId: result2.success ? args.session_id || result2.sessionId : args.session_id,
|
|
2898
|
+
...result2.success ? { source: "direct", taskId } : {},
|
|
2899
|
+
taskMode,
|
|
2900
|
+
...result2.success && result2.providerType ? { providerType: result2.providerType } : {},
|
|
2901
|
+
dispatched: result2.success === true
|
|
2902
|
+
});
|
|
1901
2903
|
}
|
|
1902
2904
|
if (args.session_id && isLocalTransport(ctx.transport)) {
|
|
1903
|
-
const cached =
|
|
2905
|
+
const cached = getSessionMetadata(meshSessionCacheKey(args.node_id, args.session_id));
|
|
2906
|
+
let resolvedProviderType = cached?.providerType || "";
|
|
2907
|
+
if (!resolvedProviderType) {
|
|
2908
|
+
let explicitSession = explicitTargetSession;
|
|
2909
|
+
if (!explicitSession) {
|
|
2910
|
+
const statusResult = await commandForNode(ctx, node, "get_status_metadata", {});
|
|
2911
|
+
const sessions = extractStatusMetadataSessions(statusResult);
|
|
2912
|
+
explicitSession = sessions.find((session) => readSessionRecordId(session) === args.session_id);
|
|
2913
|
+
}
|
|
2914
|
+
if (!explicitSession) {
|
|
2915
|
+
return JSON.stringify({
|
|
2916
|
+
success: false,
|
|
2917
|
+
recoverable: true,
|
|
2918
|
+
code: "mesh_target_session_not_found",
|
|
2919
|
+
reason: "mesh_target_session_not_found",
|
|
2920
|
+
transport: "local_ipc",
|
|
2921
|
+
retryRecommended: true,
|
|
2922
|
+
nodeId: args.node_id,
|
|
2923
|
+
sessionId: args.session_id,
|
|
2924
|
+
error: `Local session '${args.session_id}' is not present in live status for node '${args.node_id}'.`,
|
|
2925
|
+
nextAction: `Launch a fresh session with mesh_launch_session(node_id: '${args.node_id}') or retry without session_id so Repo Mesh can target a live delegate session.`
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
if (isMeshCoordinatorSessionRecord(explicitSession)) {
|
|
2929
|
+
return JSON.stringify({
|
|
2930
|
+
success: false,
|
|
2931
|
+
recoverable: true,
|
|
2932
|
+
code: "mesh_target_session_is_coordinator",
|
|
2933
|
+
reason: "mesh_target_session_is_coordinator",
|
|
2934
|
+
nodeId: args.node_id,
|
|
2935
|
+
sessionId: args.session_id,
|
|
2936
|
+
taskMode: taskMode || "unspecified",
|
|
2937
|
+
error: `Session '${args.session_id}' is a Repo Mesh coordinator session, not a visible worker session. Launch or use a visible worker session before dispatching this task.`,
|
|
2938
|
+
nextAction: `Call mesh_launch_session for node '${args.node_id}' and then retry mesh_send_task with that worker session_id, or use mesh_enqueue_task for queue-based worker assignment.`
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
if (isUnmanagedSessionRecord(explicitSession)) {
|
|
2942
|
+
return JSON.stringify({
|
|
2943
|
+
success: false,
|
|
2944
|
+
recoverable: true,
|
|
2945
|
+
code: "mesh_target_session_unmanaged",
|
|
2946
|
+
reason: "mesh_target_session_unmanaged",
|
|
2947
|
+
nodeId: args.node_id,
|
|
2948
|
+
sessionId: args.session_id,
|
|
2949
|
+
taskMode: taskMode || "unspecified",
|
|
2950
|
+
unsafeTranscriptAlias: true,
|
|
2951
|
+
unsafeDelegateTarget: true,
|
|
2952
|
+
error: `Session '${args.session_id}' on node '${args.node_id}' has no Repo Mesh delegation metadata (missing meshNodeFor/meshCoordinatorFor/launchedByCoordinator). It may be the coordinator's own session or an unrelated session \u2014 dispatching risks self-send and orphaned completion events that never reach the coordinator ledger.`,
|
|
2953
|
+
nextAction: `Call mesh_launch_session for node '${args.node_id}' to start a fresh managed worker session, then retry mesh_send_task with the returned session_id. Alternatively use mesh_enqueue_task for queue-based assignment without specifying session_id.`
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
resolvedProviderType = resolveSessionProviderType(explicitSession);
|
|
2957
|
+
if (resolvedProviderType) {
|
|
2958
|
+
meshSessionProviderMetadata.set(meshSessionCacheKey(args.node_id, args.session_id), {
|
|
2959
|
+
providerType: resolvedProviderType,
|
|
2960
|
+
providerSessionId: readString(explicitSession?.providerSessionId) || void 0,
|
|
2961
|
+
expiresAt: Date.now() + SESSION_PROVIDER_METADATA_TTL_MS
|
|
2962
|
+
});
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
if (!resolvedProviderType) {
|
|
2966
|
+
return JSON.stringify({
|
|
2967
|
+
success: false,
|
|
2968
|
+
recoverable: true,
|
|
2969
|
+
code: "mesh_target_session_provider_unknown",
|
|
2970
|
+
reason: "mesh_target_session_provider_unknown",
|
|
2971
|
+
transport: "local_ipc",
|
|
2972
|
+
retryRecommended: false,
|
|
2973
|
+
nodeId: args.node_id,
|
|
2974
|
+
sessionId: args.session_id,
|
|
2975
|
+
error: `Local session '${args.session_id}' is live but does not expose providerType/cliType, so agent_command cannot be routed safely.`,
|
|
2976
|
+
nextAction: `Relaunch the target session on node '${args.node_id}' or retry without session_id so Repo Mesh can pick a session with provider metadata.`
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
const sessionWasIdle = explicitTargetSession ? isIdleSessionRecord(explicitTargetSession) : false;
|
|
2980
|
+
const taskId = (0, import_node_crypto.randomUUID)();
|
|
2981
|
+
const dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1904
2982
|
const dispatchResult = await commandForNode(ctx, node, "agent_command", {
|
|
1905
2983
|
targetSessionId: args.session_id,
|
|
1906
|
-
|
|
2984
|
+
agentType: resolvedProviderType,
|
|
2985
|
+
cliType: resolvedProviderType,
|
|
2986
|
+
providerType: resolvedProviderType,
|
|
1907
2987
|
action: "send_chat",
|
|
1908
|
-
message: args.message
|
|
2988
|
+
message: args.message,
|
|
2989
|
+
meshContext: {
|
|
2990
|
+
meshId: ctx.mesh.id,
|
|
2991
|
+
nodeId: args.node_id,
|
|
2992
|
+
taskId
|
|
2993
|
+
}
|
|
1909
2994
|
});
|
|
1910
2995
|
const dispatchPayload = unwrapCommandPayload(dispatchResult);
|
|
1911
2996
|
if (dispatchPayload?.success === false || dispatchResult?.success === false) {
|
|
2997
|
+
const source = dispatchPayload?.success === false ? dispatchPayload : dispatchResult;
|
|
1912
2998
|
return JSON.stringify({
|
|
2999
|
+
...source && typeof source === "object" ? source : {},
|
|
1913
3000
|
success: false,
|
|
1914
3001
|
nodeId: args.node_id,
|
|
1915
3002
|
sessionId: args.session_id,
|
|
@@ -1921,23 +3008,61 @@ async function meshSendTask(ctx, args) {
|
|
|
1921
3008
|
kind: "task_dispatched",
|
|
1922
3009
|
nodeId: args.node_id,
|
|
1923
3010
|
sessionId: args.session_id,
|
|
1924
|
-
providerType:
|
|
1925
|
-
payload:
|
|
3011
|
+
providerType: resolvedProviderType,
|
|
3012
|
+
payload: buildDirectTaskPayload(args.message, "local_direct", {
|
|
3013
|
+
taskId,
|
|
3014
|
+
taskMode,
|
|
3015
|
+
providerType: resolvedProviderType,
|
|
3016
|
+
targetSessionId: args.session_id,
|
|
3017
|
+
dispatchedToIdleSession: sessionWasIdle
|
|
3018
|
+
})
|
|
1926
3019
|
});
|
|
1927
3020
|
} catch {
|
|
1928
3021
|
}
|
|
1929
|
-
|
|
3022
|
+
(0, import_daemon_core.insertDirectDispatch)(ctx.mesh.id, {
|
|
3023
|
+
taskId,
|
|
3024
|
+
nodeId: args.node_id,
|
|
3025
|
+
sessionId: args.session_id,
|
|
3026
|
+
providerType: resolvedProviderType || void 0,
|
|
3027
|
+
message: args.message,
|
|
3028
|
+
taskMode: taskMode || void 0,
|
|
3029
|
+
via: "local_direct",
|
|
3030
|
+
dispatchedToIdleSession: sessionWasIdle,
|
|
3031
|
+
dispatchedAt
|
|
3032
|
+
});
|
|
3033
|
+
return JSON.stringify({
|
|
3034
|
+
success: true,
|
|
3035
|
+
dispatched: true,
|
|
3036
|
+
source: "direct",
|
|
3037
|
+
taskId,
|
|
3038
|
+
taskMode,
|
|
3039
|
+
providerType: resolvedProviderType,
|
|
3040
|
+
nodeId: args.node_id,
|
|
3041
|
+
sessionId: args.session_id,
|
|
3042
|
+
...sessionWasIdle ? {
|
|
3043
|
+
dispatchAcknowledgementRisk: true,
|
|
3044
|
+
dispatchAcknowledgementRiskReason: "session_was_idle_at_dispatch",
|
|
3045
|
+
dispatchAcknowledgementNote: `Session '${args.session_id}' was idle at dispatch time. If it does not transition to generating, this direct task was not acknowledged. Use mesh_status to verify; if the session remains idle, it may appear as stale direct work \u2014 launch a fresh session and retry.`
|
|
3046
|
+
} : {}
|
|
3047
|
+
});
|
|
1930
3048
|
}
|
|
1931
3049
|
const task = (0, import_daemon_core.enqueueTask)(ctx.mesh.id, args.message, {
|
|
1932
3050
|
targetNodeId: args.node_id,
|
|
1933
|
-
targetSessionId: args.session_id
|
|
3051
|
+
targetSessionId: args.session_id,
|
|
3052
|
+
taskMode
|
|
1934
3053
|
});
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
3054
|
+
const queueTrigger = isLocalTransport(ctx.transport) || ctx.transport instanceof IpcTransport ? await triggerMeshQueueAndReport(ctx) : void 0;
|
|
3055
|
+
const pendingEvents = isLocalTransport(ctx.transport) ? (0, import_daemon_core.drainPendingMeshCoordinatorEvents)(ctx.mesh.id, ctx.localDaemonId) : [];
|
|
3056
|
+
const result = {
|
|
3057
|
+
success: true,
|
|
3058
|
+
source: "queue",
|
|
3059
|
+
nodeId: args.node_id,
|
|
3060
|
+
taskId: task.id,
|
|
3061
|
+
status: task.status,
|
|
3062
|
+
taskMode: task.taskMode,
|
|
3063
|
+
queueTrigger,
|
|
3064
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
3065
|
+
};
|
|
1941
3066
|
if (pendingEvents.length > 0) {
|
|
1942
3067
|
result.pendingCoordinatorEvents = pendingEvents;
|
|
1943
3068
|
}
|
|
@@ -1961,7 +3086,7 @@ async function meshReadChat(ctx, args) {
|
|
|
1961
3086
|
await drainCoordinatorPendingEvents(ctx, { nodeIds: [args.node_id] });
|
|
1962
3087
|
}
|
|
1963
3088
|
if (isLocalTransport(ctx.transport)) {
|
|
1964
|
-
const cached =
|
|
3089
|
+
const cached = resolveMeshSessionProviderMetadata(ctx, args.node_id, args.session_id);
|
|
1965
3090
|
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
1966
3091
|
const result = await commandForNode(ctx, node, "read_chat", {
|
|
1967
3092
|
sessionId: args.session_id,
|
|
@@ -1969,18 +3094,19 @@ async function meshReadChat(ctx, args) {
|
|
|
1969
3094
|
workspace: node.workspace,
|
|
1970
3095
|
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
1971
3096
|
...providerSessionId ? { providerSessionId } : {},
|
|
1972
|
-
tailLimit: args.tail ??
|
|
3097
|
+
tailLimit: args.tail ?? 3
|
|
1973
3098
|
});
|
|
1974
3099
|
const payload = annotateRapidReadChatAdvisory(unwrapCommandPayload(result), {
|
|
1975
3100
|
key: `mesh:${args.node_id}:${args.session_id}`,
|
|
1976
3101
|
toolName: "mesh_read_chat",
|
|
1977
3102
|
completionCallbackExpected: true
|
|
1978
3103
|
});
|
|
1979
|
-
|
|
3104
|
+
const useCompact = args.compact !== false;
|
|
3105
|
+
if (useCompact) {
|
|
1980
3106
|
const compactPayload = compactChatPayload(payload, {
|
|
1981
3107
|
nodeId: args.node_id,
|
|
1982
3108
|
sessionId: args.session_id,
|
|
1983
|
-
limit: args.tail ??
|
|
3109
|
+
limit: args.tail ?? 3
|
|
1984
3110
|
});
|
|
1985
3111
|
return JSON.stringify(
|
|
1986
3112
|
payload.pollingAdvisory ? { ...compactPayload, pollingAdvisory: payload.pollingAdvisory } : compactPayload,
|
|
@@ -1993,7 +3119,7 @@ async function meshReadChat(ctx, args) {
|
|
|
1993
3119
|
try {
|
|
1994
3120
|
const targetId = `${node.daemonId}:session:${args.session_id}`;
|
|
1995
3121
|
const res = await ctx.transport.readChat(targetId, {
|
|
1996
|
-
limit: args.tail ??
|
|
3122
|
+
limit: args.tail ?? 3,
|
|
1997
3123
|
sessionId: args.session_id
|
|
1998
3124
|
});
|
|
1999
3125
|
return JSON.stringify(res, null, 2);
|
|
@@ -2007,7 +3133,7 @@ async function meshReadChat(ctx, args) {
|
|
|
2007
3133
|
async function meshReadDebug(ctx, args) {
|
|
2008
3134
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2009
3135
|
if (isLocalTransport(ctx.transport)) {
|
|
2010
|
-
const cached =
|
|
3136
|
+
const cached = resolveMeshSessionProviderMetadata(ctx, args.node_id, args.session_id);
|
|
2011
3137
|
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
2012
3138
|
const delivery = args.delivery === "inline" ? void 0 : "daemon_file";
|
|
2013
3139
|
const result = await commandForNode(ctx, node, "get_chat_debug_bundle", {
|
|
@@ -2038,6 +3164,8 @@ async function meshReadDebug(ctx, args) {
|
|
|
2038
3164
|
}
|
|
2039
3165
|
async function meshLaunchSession(ctx, args) {
|
|
2040
3166
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3167
|
+
const bootstrapBlock = getWorktreeBootstrapLaunchBlock(node);
|
|
3168
|
+
if (bootstrapBlock) return JSON.stringify(bootstrapBlock, null, 2);
|
|
2041
3169
|
if (isLocalTransport(ctx.transport)) {
|
|
2042
3170
|
let resolvedProviderType = typeof args.type === "string" && args.type.trim() ? args.type : "";
|
|
2043
3171
|
if (!resolvedProviderType) {
|
|
@@ -2062,6 +3190,10 @@ async function meshLaunchSession(ctx, args) {
|
|
|
2062
3190
|
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
2063
3191
|
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
2064
3192
|
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
3193
|
+
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
3194
|
+
if (node.daemonId && !isLocalNode && !coordinatorDaemonId) {
|
|
3195
|
+
return JSON.stringify(buildMissingCoordinatorDaemonIdFailure(ctx, node, resolvedProviderType), null, 2);
|
|
3196
|
+
}
|
|
2065
3197
|
let result;
|
|
2066
3198
|
try {
|
|
2067
3199
|
result = await commandForNode(ctx, node, "launch_cli", {
|
|
@@ -2089,7 +3221,8 @@ async function meshLaunchSession(ctx, args) {
|
|
|
2089
3221
|
if (runtimeSessionId) {
|
|
2090
3222
|
meshSessionProviderMetadata.set(meshSessionCacheKey(args.node_id, runtimeSessionId), {
|
|
2091
3223
|
providerType: resolvedProviderType,
|
|
2092
|
-
...providerSessionId ? { providerSessionId } : {}
|
|
3224
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3225
|
+
expiresAt: Date.now() + SESSION_PROVIDER_METADATA_TTL_MS
|
|
2093
3226
|
});
|
|
2094
3227
|
}
|
|
2095
3228
|
try {
|
|
@@ -2102,18 +3235,13 @@ async function meshLaunchSession(ctx, args) {
|
|
|
2102
3235
|
});
|
|
2103
3236
|
} catch {
|
|
2104
3237
|
}
|
|
2105
|
-
const
|
|
2106
|
-
if (ctx.transport instanceof IpcTransport && node.daemonId && !isLocalNode) {
|
|
2107
|
-
ctx.transport.meshCommand(node.daemonId, "trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
2108
|
-
});
|
|
2109
|
-
} else if (isLocalTransport(ctx.transport)) {
|
|
2110
|
-
ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
2111
|
-
});
|
|
2112
|
-
}
|
|
3238
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx, node, { localNode: isLocalNode });
|
|
2113
3239
|
return JSON.stringify({
|
|
2114
3240
|
...launchPayload,
|
|
2115
3241
|
resolvedProviderType,
|
|
2116
|
-
...providerSessionId ? { providerSessionId } : {}
|
|
3242
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3243
|
+
queueTrigger,
|
|
3244
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
2117
3245
|
}, null, 2);
|
|
2118
3246
|
} else if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2119
3247
|
let resolvedProviderType = typeof args.type === "string" && args.type.trim() ? args.type : "";
|
|
@@ -2127,6 +3255,9 @@ async function meshLaunchSession(ctx, args) {
|
|
|
2127
3255
|
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
2128
3256
|
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
2129
3257
|
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
3258
|
+
if (!coordinatorDaemonId) {
|
|
3259
|
+
return JSON.stringify(buildMissingCoordinatorDaemonIdFailure(ctx, node, resolvedProviderType), null, 2);
|
|
3260
|
+
}
|
|
2130
3261
|
try {
|
|
2131
3262
|
const res = await ctx.transport.launch(node.daemonId, {
|
|
2132
3263
|
type: resolvedProviderType,
|
|
@@ -2207,6 +3338,51 @@ async function meshGitStatus(ctx, args) {
|
|
|
2207
3338
|
}, null, 2);
|
|
2208
3339
|
}
|
|
2209
3340
|
}
|
|
3341
|
+
async function meshFastForwardNode(ctx, args) {
|
|
3342
|
+
await refreshMeshFromDaemon(ctx);
|
|
3343
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3344
|
+
const submoduleIgnorePaths = node.policy?.submoduleIgnorePaths || [];
|
|
3345
|
+
if (node.policy?.readOnly) {
|
|
3346
|
+
return JSON.stringify({
|
|
3347
|
+
success: false,
|
|
3348
|
+
code: "node_read_only",
|
|
3349
|
+
nodeId: args.node_id,
|
|
3350
|
+
workspace: node.workspace,
|
|
3351
|
+
allowed: false,
|
|
3352
|
+
willRun: false,
|
|
3353
|
+
executed: false,
|
|
3354
|
+
blockingReasons: ["node_read_only"]
|
|
3355
|
+
}, null, 2);
|
|
3356
|
+
}
|
|
3357
|
+
try {
|
|
3358
|
+
const dryRun = args.dry_run === true || args.execute !== true;
|
|
3359
|
+
const result = await commandForNode(ctx, node, "fast_forward_mesh_node", {
|
|
3360
|
+
meshId: ctx.mesh.id,
|
|
3361
|
+
nodeId: node.id,
|
|
3362
|
+
workspace: node.workspace,
|
|
3363
|
+
branch: typeof args.branch === "string" ? args.branch : void 0,
|
|
3364
|
+
execute: args.execute === true && args.dry_run !== true,
|
|
3365
|
+
dryRun,
|
|
3366
|
+
updateSubmodules: args.update_submodules === true,
|
|
3367
|
+
submoduleIgnorePaths: submoduleIgnorePaths.length > 0 ? submoduleIgnorePaths : void 0
|
|
3368
|
+
});
|
|
3369
|
+
return JSON.stringify(unwrapCommandPayload(result), null, 2);
|
|
3370
|
+
} catch (e) {
|
|
3371
|
+
const failure = buildCoordinatorP2pRelayFailure(e, {
|
|
3372
|
+
command: "fast_forward_mesh_node",
|
|
3373
|
+
targetDaemonId: node.daemonId,
|
|
3374
|
+
nodeId: args.node_id
|
|
3375
|
+
});
|
|
3376
|
+
return JSON.stringify({
|
|
3377
|
+
...failure,
|
|
3378
|
+
workspace: node.workspace,
|
|
3379
|
+
allowed: false,
|
|
3380
|
+
willRun: false,
|
|
3381
|
+
executed: false,
|
|
3382
|
+
blockingReasons: [failure.code || "mesh_fast_forward_unavailable"]
|
|
3383
|
+
}, null, 2);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
2210
3386
|
async function meshCheckpoint(ctx, args) {
|
|
2211
3387
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2212
3388
|
if (node.policy?.readOnly) {
|
|
@@ -2222,7 +3398,13 @@ async function meshCheckpoint(ctx, args) {
|
|
|
2222
3398
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
2223
3399
|
kind: "checkpoint_created",
|
|
2224
3400
|
nodeId: args.node_id,
|
|
2225
|
-
payload: {
|
|
3401
|
+
payload: {
|
|
3402
|
+
message: args.message,
|
|
3403
|
+
commit: result?.checkpoint?.commit,
|
|
3404
|
+
outcome: result?.checkpoint?.status || (result?.checkpoint?.noop ? "skipped" : void 0),
|
|
3405
|
+
noop: result?.checkpoint?.noop === true,
|
|
3406
|
+
reason: result?.checkpoint?.reason
|
|
3407
|
+
}
|
|
2226
3408
|
});
|
|
2227
3409
|
} catch {
|
|
2228
3410
|
}
|
|
@@ -2238,7 +3420,13 @@ async function meshCheckpoint(ctx, args) {
|
|
|
2238
3420
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
2239
3421
|
kind: "checkpoint_created",
|
|
2240
3422
|
nodeId: args.node_id,
|
|
2241
|
-
payload: {
|
|
3423
|
+
payload: {
|
|
3424
|
+
message: args.message,
|
|
3425
|
+
commit: res?.checkpoint?.commit,
|
|
3426
|
+
outcome: res?.checkpoint?.status || (res?.checkpoint?.noop ? "skipped" : void 0),
|
|
3427
|
+
noop: res?.checkpoint?.noop === true,
|
|
3428
|
+
reason: res?.checkpoint?.reason
|
|
3429
|
+
}
|
|
2242
3430
|
});
|
|
2243
3431
|
} catch {
|
|
2244
3432
|
}
|
|
@@ -2253,7 +3441,7 @@ async function meshCheckpoint(ctx, args) {
|
|
|
2253
3441
|
async function meshApprove(ctx, args) {
|
|
2254
3442
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2255
3443
|
if (isLocalTransport(ctx.transport)) {
|
|
2256
|
-
const cached =
|
|
3444
|
+
const cached = getSessionMetadata(meshSessionCacheKey(args.node_id, args.session_id));
|
|
2257
3445
|
const providerSessionId = cached?.providerSessionId;
|
|
2258
3446
|
const result = await commandForNode(ctx, node, "resolve_action", {
|
|
2259
3447
|
sessionId: args.session_id,
|
|
@@ -2406,6 +3594,43 @@ async function meshRemoveNode(ctx, args) {
|
|
|
2406
3594
|
return JSON.stringify({ error: "Cloud mesh remove_node requires node daemonId" });
|
|
2407
3595
|
}
|
|
2408
3596
|
}
|
|
3597
|
+
function resolveRefineConfigNode(ctx, nodeId) {
|
|
3598
|
+
if (nodeId) return findNode(ctx.mesh, nodeId);
|
|
3599
|
+
const node = ctx.mesh.nodes.find((entry) => !!entry.workspace);
|
|
3600
|
+
if (!node) throw new Error("No mesh node with a workspace is available");
|
|
3601
|
+
return node;
|
|
3602
|
+
}
|
|
3603
|
+
async function meshRefineConfigSchema(ctx) {
|
|
3604
|
+
const node = resolveRefineConfigNode(ctx);
|
|
3605
|
+
const result = await commandForNode(ctx, node, "get_mesh_refine_config_schema", {});
|
|
3606
|
+
return JSON.stringify(result, null, 2);
|
|
3607
|
+
}
|
|
3608
|
+
async function meshValidateRefineConfig(ctx, args) {
|
|
3609
|
+
const node = resolveRefineConfigNode(ctx, args.node_id);
|
|
3610
|
+
const result = await commandForNode(ctx, node, "validate_mesh_refine_config", {
|
|
3611
|
+
workspace: node.workspace,
|
|
3612
|
+
inlineMesh: ctx.mesh,
|
|
3613
|
+
...args.config ? { config: args.config } : {}
|
|
3614
|
+
});
|
|
3615
|
+
return JSON.stringify(result, null, 2);
|
|
3616
|
+
}
|
|
3617
|
+
async function meshSuggestRefineConfig(ctx, args) {
|
|
3618
|
+
const node = resolveRefineConfigNode(ctx, args.node_id);
|
|
3619
|
+
const result = await commandForNode(ctx, node, "suggest_mesh_refine_config", {
|
|
3620
|
+
workspace: node.workspace,
|
|
3621
|
+
inlineMesh: ctx.mesh
|
|
3622
|
+
});
|
|
3623
|
+
return JSON.stringify(result, null, 2);
|
|
3624
|
+
}
|
|
3625
|
+
async function meshRefinePlan(ctx, args) {
|
|
3626
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3627
|
+
const result = await commandForNode(ctx, node, "plan_mesh_refine_node", {
|
|
3628
|
+
meshId: ctx.mesh.id,
|
|
3629
|
+
nodeId: args.node_id,
|
|
3630
|
+
inlineMesh: ctx.mesh
|
|
3631
|
+
});
|
|
3632
|
+
return JSON.stringify(result, null, 2);
|
|
3633
|
+
}
|
|
2409
3634
|
async function meshRefineNode(ctx, args) {
|
|
2410
3635
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2411
3636
|
if (isLocalTransport(ctx.transport)) {
|
|
@@ -2414,7 +3639,7 @@ async function meshRefineNode(ctx, args) {
|
|
|
2414
3639
|
nodeId: args.node_id,
|
|
2415
3640
|
inlineMesh: ctx.mesh
|
|
2416
3641
|
});
|
|
2417
|
-
if (result?.success && result.removeResult?.removed !== false) {
|
|
3642
|
+
if (result?.success && result.async !== true && result.removeResult?.removed !== false) {
|
|
2418
3643
|
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
2419
3644
|
if (idx >= 0) {
|
|
2420
3645
|
ctx.mesh.nodes.splice(idx, 1);
|
|
@@ -2429,7 +3654,7 @@ async function meshRefineNode(ctx, args) {
|
|
|
2429
3654
|
nodeId: args.node_id,
|
|
2430
3655
|
inlineMesh: ctx.mesh
|
|
2431
3656
|
});
|
|
2432
|
-
if (res?.success && res.removeResult?.removed !== false) {
|
|
3657
|
+
if (res?.success && res.async !== true && res.removeResult?.removed !== false) {
|
|
2433
3658
|
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
2434
3659
|
if (idx >= 0) {
|
|
2435
3660
|
ctx.mesh.nodes.splice(idx, 1);
|
|
@@ -2466,13 +3691,13 @@ var STANDARD_TOOLS = [
|
|
|
2466
3691
|
function buildMcpHelpText() {
|
|
2467
3692
|
const meshTools = ALL_MESH_TOOLS.map((tool) => tool.name);
|
|
2468
3693
|
return `
|
|
2469
|
-
|
|
3694
|
+
ADHDev MCP Server
|
|
2470
3695
|
|
|
2471
3696
|
Usage:
|
|
2472
|
-
adhdev
|
|
2473
|
-
adhdev
|
|
2474
|
-
adhdev
|
|
2475
|
-
adhdev-mcp --
|
|
3697
|
+
adhdev mcp Local mode (requires standalone daemon)
|
|
3698
|
+
adhdev mcp --api-key <key> Cloud mode (ADHDev cloud API)
|
|
3699
|
+
adhdev mcp --mode ipc --repo-mesh <mesh_id> Cloud daemon IPC mesh mode
|
|
3700
|
+
adhdev-mcp --help Compatibility bin (same server, legacy package entrypoint)
|
|
2476
3701
|
|
|
2477
3702
|
Options:
|
|
2478
3703
|
--mode <mode> Transport: local, cloud, or ipc
|
|
@@ -2497,6 +3722,7 @@ Mesh tools: ${meshTools.join(", ")}
|
|
|
2497
3722
|
// src/server.ts
|
|
2498
3723
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
2499
3724
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3725
|
+
var import_node_os = __toESM(require("os"));
|
|
2500
3726
|
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
2501
3727
|
|
|
2502
3728
|
// src/transports/local.ts
|
|
@@ -3048,11 +4274,17 @@ function formatChatResult(result, sessionId, format, limit = 50, compact = false
|
|
|
3048
4274
|
}, null, 2);
|
|
3049
4275
|
}
|
|
3050
4276
|
if ((format === "text" || format === void 0) && compact && compactPayload) {
|
|
3051
|
-
const
|
|
4277
|
+
const summaryText = typeof compactPayload.summary === "string" ? compactPayload.summary.trim() : "";
|
|
4278
|
+
const tail = outputMessages.slice(-limit);
|
|
4279
|
+
const lastIndex = tail.length - 1;
|
|
4280
|
+
const lines2 = tail.flatMap((m, idx) => {
|
|
3052
4281
|
const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
|
|
3053
4282
|
const content = messageContent(m);
|
|
4283
|
+
if (idx === lastIndex && (role === "Agent" || m.role === "agent") && summaryText && content.trim() === summaryText) {
|
|
4284
|
+
return [];
|
|
4285
|
+
}
|
|
3054
4286
|
const truncated = content.length > 500 ? `${content.slice(0, 500)}\u2026` : content;
|
|
3055
|
-
return `[${role}] ${truncated}
|
|
4287
|
+
return [`[${role}] ${truncated}`];
|
|
3056
4288
|
});
|
|
3057
4289
|
if (compactPayload.summary) {
|
|
3058
4290
|
const truncatedSummary = compactPayload.summary.length > 500 ? `${compactPayload.summary.slice(0, 500)}\u2026` : compactPayload.summary;
|
|
@@ -4030,6 +5262,7 @@ async function startMcpServer(opts) {
|
|
|
4030
5262
|
requirePreTaskCheckpoint: false,
|
|
4031
5263
|
requirePostTaskCheckpoint: true,
|
|
4032
5264
|
requireApprovalForPush: true,
|
|
5265
|
+
allowAutoPublishSubmoduleMainCommits: false,
|
|
4033
5266
|
requireApprovalForDestructiveGit: true,
|
|
4034
5267
|
dirtyWorkspaceBehavior: "warn",
|
|
4035
5268
|
maxParallelTasks: 2,
|
|
@@ -4086,11 +5319,13 @@ async function startMcpServer(opts) {
|
|
|
4086
5319
|
}
|
|
4087
5320
|
let localDaemonId;
|
|
4088
5321
|
let localMachineId;
|
|
5322
|
+
let coordinatorHostname = import_node_os.default.hostname();
|
|
4089
5323
|
if (transport instanceof LocalTransport || transport instanceof IpcTransport) {
|
|
4090
5324
|
try {
|
|
4091
5325
|
const { loadConfig } = await import("@adhdev/daemon-core");
|
|
4092
5326
|
const cfg = loadConfig();
|
|
4093
|
-
if (cfg.
|
|
5327
|
+
if (cfg.machineId) localMachineId = cfg.machineId;
|
|
5328
|
+
else if (cfg.registeredMachineId) localMachineId = cfg.registeredMachineId;
|
|
4094
5329
|
} catch {
|
|
4095
5330
|
}
|
|
4096
5331
|
}
|
|
@@ -4098,14 +5333,16 @@ async function startMcpServer(opts) {
|
|
|
4098
5333
|
try {
|
|
4099
5334
|
const statusResult = await transport.getStatus();
|
|
4100
5335
|
const instanceId = typeof statusResult?.status?.instanceId === "string" ? statusResult.status.instanceId.trim() : "";
|
|
5336
|
+
const hostname = typeof statusResult?.status?.hostname === "string" ? statusResult.status.hostname.trim() : typeof statusResult?.status?.machine?.hostname === "string" ? statusResult.status.machine.hostname.trim() : "";
|
|
4101
5337
|
if (instanceId) localDaemonId = instanceId;
|
|
5338
|
+
if (hostname) coordinatorHostname = hostname;
|
|
4102
5339
|
} catch {
|
|
4103
5340
|
}
|
|
4104
5341
|
}
|
|
4105
|
-
const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {}, ...localMachineId ? { localMachineId } : {} };
|
|
5342
|
+
const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {}, ...localMachineId ? { localMachineId } : {}, ...coordinatorHostname ? { coordinatorHostname } : {} };
|
|
4106
5343
|
const coordinatorPrompt = await buildMeshModeCoordinatorPrompt(mesh);
|
|
4107
5344
|
const server2 = new import_server.Server(
|
|
4108
|
-
{ name: "adhdev-mcp-server", version: "0.9.
|
|
5345
|
+
{ name: "adhdev-mcp-server", version: "0.9.82" },
|
|
4109
5346
|
{ capabilities: { tools: {}, resources: {} } }
|
|
4110
5347
|
);
|
|
4111
5348
|
const { ListResourcesRequestSchema, ReadResourceRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
@@ -4131,7 +5368,7 @@ async function startMcpServer(opts) {
|
|
|
4131
5368
|
let text;
|
|
4132
5369
|
switch (name) {
|
|
4133
5370
|
case "mesh_status":
|
|
4134
|
-
text = await meshStatus(meshCtx);
|
|
5371
|
+
text = await meshStatus(meshCtx, a);
|
|
4135
5372
|
break;
|
|
4136
5373
|
case "mesh_list_nodes":
|
|
4137
5374
|
text = await meshListNodes(meshCtx);
|
|
@@ -4163,6 +5400,9 @@ async function startMcpServer(opts) {
|
|
|
4163
5400
|
case "mesh_git_status":
|
|
4164
5401
|
text = await meshGitStatus(meshCtx, a);
|
|
4165
5402
|
break;
|
|
5403
|
+
case "mesh_fast_forward_node":
|
|
5404
|
+
text = await meshFastForwardNode(meshCtx, a);
|
|
5405
|
+
break;
|
|
4166
5406
|
case "mesh_checkpoint":
|
|
4167
5407
|
text = await meshCheckpoint(meshCtx, a);
|
|
4168
5408
|
break;
|
|
@@ -4178,6 +5418,18 @@ async function startMcpServer(opts) {
|
|
|
4178
5418
|
case "mesh_refine_node":
|
|
4179
5419
|
text = await meshRefineNode(meshCtx, a);
|
|
4180
5420
|
break;
|
|
5421
|
+
case "mesh_refine_config_schema":
|
|
5422
|
+
text = await meshRefineConfigSchema(meshCtx);
|
|
5423
|
+
break;
|
|
5424
|
+
case "mesh_validate_refine_config":
|
|
5425
|
+
text = await meshValidateRefineConfig(meshCtx, a);
|
|
5426
|
+
break;
|
|
5427
|
+
case "mesh_suggest_refine_config":
|
|
5428
|
+
text = await meshSuggestRefineConfig(meshCtx, a);
|
|
5429
|
+
break;
|
|
5430
|
+
case "mesh_refine_plan":
|
|
5431
|
+
text = await meshRefinePlan(meshCtx, a);
|
|
5432
|
+
break;
|
|
4181
5433
|
case "mesh_cleanup_sessions":
|
|
4182
5434
|
text = await meshCleanupSessions(meshCtx, a);
|
|
4183
5435
|
break;
|