@adhdev/daemon-standalone 0.9.82-rc.2 → 0.9.82-rc.200
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 +56516 -26116
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/public/assets/index-BmDLCbzV.css +1 -0
- package/public/assets/index-DrnKfl-h.js +107 -0
- package/public/assets/{terminal-Cz61jYPm.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 +1745 -201
- 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-01wE493H.css +0 -1
- package/public/assets/index-BU3NAjAr.js +0 -98
- package/public/assets/vendor-CLec0455.js +0 -2723
|
@@ -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
|
};
|
|
@@ -161,16 +248,19 @@ function isCoordinatorVisibleMessage(message) {
|
|
|
161
248
|
if (meta?.internal === true || meta?.debug === true || meta?.control === true || meta?.userVisible === false || meta?.user_visible === false) return false;
|
|
162
249
|
return role === "user" || role === "assistant" || role === "agent";
|
|
163
250
|
}
|
|
251
|
+
function buildCompactMessageTail(visibleMessages, opts) {
|
|
252
|
+
return visibleMessages.slice(-opts.limit);
|
|
253
|
+
}
|
|
164
254
|
function compactChatPayload(payload, opts = {}) {
|
|
165
255
|
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
166
256
|
const visible = rawMessages.filter(isCoordinatorVisibleMessage);
|
|
167
257
|
const limit = Math.max(1, Math.min(opts.limit ?? 10, 10));
|
|
168
|
-
const messages = visible.slice(-limit);
|
|
169
258
|
const finalAssistant = [...visible].reverse().find((message) => {
|
|
170
259
|
const role = String(message?.role ?? "").toLowerCase();
|
|
171
260
|
return (role === "assistant" || role === "agent") && messageContent(message).trim();
|
|
172
261
|
});
|
|
173
262
|
const summary = typeof payload?.summary === "string" && payload.summary.trim() ? payload.summary.trim() : messageContent(finalAssistant).trim();
|
|
263
|
+
const messages = buildCompactMessageTail(visible, { summary, finalAssistant, limit });
|
|
174
264
|
return {
|
|
175
265
|
success: payload?.success !== false,
|
|
176
266
|
compact: true,
|
|
@@ -231,30 +321,86 @@ function annotateRapidReadChatAdvisory(payload, options) {
|
|
|
231
321
|
|
|
232
322
|
// src/tools/mesh-tools.ts
|
|
233
323
|
var import_daemon_core = require("@adhdev/daemon-core");
|
|
324
|
+
var SESSION_PROVIDER_METADATA_TTL_MS = 30 * 6e4;
|
|
234
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
|
+
}
|
|
235
347
|
function readString(value) {
|
|
236
348
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
237
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
|
+
}
|
|
238
375
|
var DUPLICATE_DISPATCH_WINDOW_MS = 6e4;
|
|
239
376
|
var STALE_ASSIGNED_QUEUE_MS = 30 * 6e4;
|
|
240
377
|
var OLD_HISTORICAL_QUEUE_RECORD_MS = 7 * 24 * 60 * 6e4;
|
|
241
378
|
var ACTIVE_QUEUE_STATUSES = /* @__PURE__ */ new Set(["pending", "assigned"]);
|
|
242
379
|
var HISTORICAL_QUEUE_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
243
380
|
async function refreshMeshFromDaemon(ctx) {
|
|
244
|
-
if (!(ctx.transport
|
|
381
|
+
if (!isLocalTransport(ctx.transport)) return;
|
|
245
382
|
try {
|
|
246
383
|
const result = await ctx.transport.command("get_mesh", { meshId: ctx.mesh.id });
|
|
247
384
|
if (!result?.success || !Array.isArray(result.mesh?.nodes)) return;
|
|
248
385
|
const refreshedNodes = result.mesh.nodes.filter((n) => n?.id).map((n) => n);
|
|
249
|
-
if (!refreshedNodes.length) return;
|
|
250
386
|
ctx.mesh.nodes.splice(0, ctx.mesh.nodes.length, ...refreshedNodes);
|
|
251
387
|
ctx.mesh.updatedAt = result.mesh.updatedAt ?? ctx.mesh.updatedAt;
|
|
252
388
|
} catch {
|
|
253
389
|
}
|
|
254
390
|
}
|
|
391
|
+
async function syncCoordinatorDaemonMeshCache(ctx) {
|
|
392
|
+
if (!(ctx.transport instanceof IpcTransport)) return;
|
|
393
|
+
try {
|
|
394
|
+
await ctx.transport.command("get_mesh", {
|
|
395
|
+
meshId: ctx.mesh.id,
|
|
396
|
+
inlineMesh: ctx.mesh
|
|
397
|
+
});
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
}
|
|
255
401
|
async function findNodeWithRefresh(ctx, nodeId) {
|
|
256
402
|
const hit = ctx.mesh.nodes.find((n) => n.id === nodeId);
|
|
257
|
-
if (hit) return hit;
|
|
403
|
+
if (hit && !hit.isLocalWorktree) return hit;
|
|
258
404
|
await refreshMeshFromDaemon(ctx);
|
|
259
405
|
const refreshed = ctx.mesh.nodes.find((n) => n.id === nodeId);
|
|
260
406
|
if (!refreshed) throw new Error(`Node '${nodeId}' is not a member of mesh '${ctx.mesh.name}'`);
|
|
@@ -262,7 +408,7 @@ async function findNodeWithRefresh(ctx, nodeId) {
|
|
|
262
408
|
}
|
|
263
409
|
async function findOptionalNodeWithRefresh(ctx, nodeId) {
|
|
264
410
|
const hit = ctx.mesh.nodes.find((n) => n.id === nodeId);
|
|
265
|
-
if (hit) return hit;
|
|
411
|
+
if (hit && !hit.isLocalWorktree) return hit;
|
|
266
412
|
await refreshMeshFromDaemon(ctx);
|
|
267
413
|
return ctx.mesh.nodes.find((n) => n.id === nodeId) ?? null;
|
|
268
414
|
}
|
|
@@ -314,9 +460,26 @@ function buildMissingNodeReadChatRecovery(ctx, args) {
|
|
|
314
460
|
readDebugLocator: readString(lastTerminal?.payload?.readDebugLocator) || readString(lastTerminal?.payload?.debugBundlePath)
|
|
315
461
|
};
|
|
316
462
|
if (finalSummary) {
|
|
463
|
+
if (args.compact === true) {
|
|
464
|
+
return {
|
|
465
|
+
...compactChatPayload({
|
|
466
|
+
success: true,
|
|
467
|
+
status: "idle",
|
|
468
|
+
providerSessionId,
|
|
469
|
+
summary: finalSummary,
|
|
470
|
+
messages: [{ role: "assistant", content: finalSummary, isHistorical: true }]
|
|
471
|
+
}, {
|
|
472
|
+
nodeId: args.node_id,
|
|
473
|
+
sessionId: args.session_id,
|
|
474
|
+
limit: args.tail ?? 10
|
|
475
|
+
}),
|
|
476
|
+
recoveredFromLedger: true,
|
|
477
|
+
ledger
|
|
478
|
+
};
|
|
479
|
+
}
|
|
317
480
|
return {
|
|
318
481
|
success: true,
|
|
319
|
-
compact:
|
|
482
|
+
compact: false,
|
|
320
483
|
recoveredFromLedger: true,
|
|
321
484
|
nodeId: args.node_id,
|
|
322
485
|
sessionId: args.session_id,
|
|
@@ -368,6 +531,33 @@ function buildMissingNodeReadChatRecovery(ctx, args) {
|
|
|
368
531
|
function readSessionRecordId(session) {
|
|
369
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);
|
|
370
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
|
+
}
|
|
371
561
|
function addSessionRecord(target, session) {
|
|
372
562
|
if (!session || typeof session !== "object" || isTerminalSessionRecord(session)) return;
|
|
373
563
|
const sessionId = readSessionRecordId(session);
|
|
@@ -436,18 +626,26 @@ function queueAssignmentStaleReason(task, liveness) {
|
|
|
436
626
|
}
|
|
437
627
|
function buildQueueStatusSummary(queue) {
|
|
438
628
|
const counts = { pending: 0, assigned: 0, completed: 0, failed: 0, cancelled: 0 };
|
|
629
|
+
let staleAssigned = 0;
|
|
439
630
|
for (const task of queue) {
|
|
440
631
|
const status = typeof task?.status === "string" ? task.status : void 0;
|
|
441
632
|
if (status && Object.prototype.hasOwnProperty.call(counts, status)) {
|
|
442
633
|
counts[status] += 1;
|
|
443
634
|
}
|
|
635
|
+
if (status === "assigned" && task?.staleAssigned === true) staleAssigned += 1;
|
|
444
636
|
}
|
|
637
|
+
const liveAssigned = Math.max(0, counts.assigned - staleAssigned);
|
|
445
638
|
return {
|
|
446
639
|
totalCount: queue.length,
|
|
447
|
-
activeCount: counts.pending +
|
|
640
|
+
activeCount: counts.pending + liveAssigned,
|
|
448
641
|
historicalCount: counts.completed + counts.failed + counts.cancelled,
|
|
449
642
|
counts,
|
|
450
643
|
activeCounts: {
|
|
644
|
+
pending: counts.pending,
|
|
645
|
+
assigned: liveAssigned
|
|
646
|
+
},
|
|
647
|
+
staleAssignedCount: staleAssigned,
|
|
648
|
+
rawActiveCounts: {
|
|
451
649
|
pending: counts.pending,
|
|
452
650
|
assigned: counts.assigned
|
|
453
651
|
},
|
|
@@ -475,6 +673,18 @@ function filterQueueForView(queue, view, statuses) {
|
|
|
475
673
|
if (view === "historical") return queue.filter((task) => HISTORICAL_QUEUE_STATUSES.has(String(task?.status || "")));
|
|
476
674
|
return queue;
|
|
477
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
|
+
}
|
|
478
688
|
function slimQueueTask(task) {
|
|
479
689
|
return {
|
|
480
690
|
id: task?.id,
|
|
@@ -568,6 +778,172 @@ function unwrapCommandPayload(value) {
|
|
|
568
778
|
}
|
|
569
779
|
return current;
|
|
570
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
|
+
}
|
|
571
947
|
function isTerminalSessionRecord(session) {
|
|
572
948
|
const status = typeof session?.status === "string" ? session.status.toLowerCase() : "";
|
|
573
949
|
const lifecycle = typeof session?.lifecycle === "string" ? session.lifecycle.toLowerCase() : "";
|
|
@@ -580,13 +956,66 @@ function isIdleSessionRecord(session) {
|
|
|
580
956
|
const chatStatus = typeof session?.activeChat?.status === "string" ? session.activeChat.status.toLowerCase() : "";
|
|
581
957
|
return status === "idle" || chatStatus === "waiting_input";
|
|
582
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
|
+
}
|
|
583
974
|
function chooseDispatchableSession(sessions, providerType, meshId, nodeId) {
|
|
584
975
|
const live = sessions.filter((session) => !isTerminalSessionRecord(session));
|
|
585
976
|
const matchingProvider = (session) => !providerType || session?.providerType === providerType || session?.cliType === providerType;
|
|
586
977
|
const meshSessions = live.filter(
|
|
587
|
-
(session) => session
|
|
978
|
+
(session) => isRelaySafeRemoteDelegateSession(session, meshId, nodeId)
|
|
588
979
|
);
|
|
589
|
-
return meshSessions.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || meshSessions.find(matchingProvider) ||
|
|
980
|
+
return meshSessions.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || meshSessions.find(matchingProvider) || void 0;
|
|
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
|
+
};
|
|
590
1019
|
}
|
|
591
1020
|
function findNestedPayload(value, predicate) {
|
|
592
1021
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -615,12 +1044,16 @@ function extractGitDiff(value) {
|
|
|
615
1044
|
}
|
|
616
1045
|
function extractSubmodules(value, ignorePaths) {
|
|
617
1046
|
const payload = unwrapCommandPayload(value);
|
|
618
|
-
const subs = payload?.submodules ?? value?.submodules;
|
|
1047
|
+
const subs = payload?.status?.submodules ?? payload?.submodules ?? value?.status?.submodules ?? value?.submodules;
|
|
619
1048
|
if (!Array.isArray(subs)) return void 0;
|
|
620
1049
|
if (ignorePaths.length === 0) return subs;
|
|
621
1050
|
const ignoreSet = new Set(ignorePaths);
|
|
622
1051
|
return subs.filter((s) => s?.path && !ignoreSet.has(s.path));
|
|
623
1052
|
}
|
|
1053
|
+
function assignFullGitSnapshot(entry, status) {
|
|
1054
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) return;
|
|
1055
|
+
entry.git = status;
|
|
1056
|
+
}
|
|
624
1057
|
function extractLaunchPayload(value) {
|
|
625
1058
|
return findNestedPayload(value, (payload) => Boolean(payload?.sessionId || payload?.id || payload?.runtimeSessionId));
|
|
626
1059
|
}
|
|
@@ -745,20 +1178,76 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
745
1178
|
let sessionId = args.session_id?.trim() || "";
|
|
746
1179
|
const providerPriorityList = Array.isArray(node.policy?.providerPriority) ? node.policy.providerPriority : [];
|
|
747
1180
|
let resolvedProviderType = args.providerType?.trim() || providerPriorityList[0] || "";
|
|
748
|
-
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) {
|
|
749
1195
|
try {
|
|
750
1196
|
const relayResult = await transport.meshCommand(daemonId, "get_status_metadata", {});
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
+
}
|
|
757
1226
|
if (!resolvedProviderType) {
|
|
758
|
-
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
|
+
}
|
|
759
1236
|
}
|
|
760
1237
|
}
|
|
761
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
|
+
}
|
|
762
1251
|
}
|
|
763
1252
|
}
|
|
764
1253
|
if (!resolvedProviderType) {
|
|
@@ -770,7 +1259,8 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
770
1259
|
agentType: resolvedProviderType,
|
|
771
1260
|
cliType: resolvedProviderType,
|
|
772
1261
|
action: "send_chat",
|
|
773
|
-
message: args.message
|
|
1262
|
+
message: args.message,
|
|
1263
|
+
...args.meshContext ? { meshContext: args.meshContext } : {}
|
|
774
1264
|
});
|
|
775
1265
|
const dispatchPayload = unwrapCommandPayload(dispatchResult);
|
|
776
1266
|
if (dispatchPayload?.success === false || dispatchResult?.success === false) {
|
|
@@ -788,7 +1278,7 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
788
1278
|
error: `P2P dispatch failed: ${errorMessage}`
|
|
789
1279
|
};
|
|
790
1280
|
}
|
|
791
|
-
return { success: true, dispatched: true, sessionId: sessionId || resolvedProviderType };
|
|
1281
|
+
return { success: true, dispatched: true, sessionId: sessionId || resolvedProviderType, providerType: resolvedProviderType };
|
|
792
1282
|
} catch (e) {
|
|
793
1283
|
const errorMessage = e?.message || String(e);
|
|
794
1284
|
return {
|
|
@@ -818,34 +1308,197 @@ function resolveCoordinatorNode(ctx) {
|
|
|
818
1308
|
return void 0;
|
|
819
1309
|
}
|
|
820
1310
|
function readNodeMachineId(node) {
|
|
821
|
-
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);
|
|
822
1312
|
}
|
|
823
1313
|
function readNodeDaemonId(node) {
|
|
824
|
-
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;
|
|
825
1410
|
}
|
|
826
1411
|
function isDirectLocalNode(ctx, node) {
|
|
827
1412
|
const machineId = readNodeMachineId(node);
|
|
828
1413
|
const daemonId = readNodeDaemonId(node);
|
|
829
1414
|
return Boolean(
|
|
830
|
-
ctx.localMachineId && machineId === ctx.localMachineId || ctx.localDaemonId && daemonId === ctx.localDaemonId
|
|
1415
|
+
ctx.localMachineId && machineId === ctx.localMachineId || ctx.localDaemonId && daemonId === ctx.localDaemonId || nodeHasLocalDaemonEvidence(ctx, node)
|
|
831
1416
|
);
|
|
832
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
|
+
}
|
|
833
1442
|
function findClonedFromNode(ctx, node) {
|
|
834
1443
|
const clonedFromNodeId = readString(node.clonedFromNodeId) || readString(node.cloned_from_node_id);
|
|
835
1444
|
if (!clonedFromNodeId) return void 0;
|
|
836
1445
|
return ctx.mesh.nodes.find((n) => n.id === clonedFromNodeId || n.nodeId === clonedFromNodeId || n.node_id === clonedFromNodeId);
|
|
837
1446
|
}
|
|
838
1447
|
function isLocalControlPlaneNode(ctx, node) {
|
|
839
|
-
|
|
840
|
-
if (node.isLocalWorktree === true) {
|
|
841
|
-
const sourceNode = findClonedFromNode(ctx, node);
|
|
842
|
-
if (sourceNode && isDirectLocalNode(ctx, sourceNode)) return true;
|
|
843
|
-
}
|
|
844
|
-
return false;
|
|
1448
|
+
return !!getLocalControlPlaneMatchReason(ctx, node);
|
|
845
1449
|
}
|
|
846
1450
|
function meshSessionCacheKey(nodeId, runtimeSessionId) {
|
|
847
1451
|
return `${nodeId}:${runtimeSessionId}`;
|
|
848
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
|
+
}
|
|
849
1502
|
function countUncommittedChanges(status) {
|
|
850
1503
|
if (typeof status?.uncommittedChanges === "number") return status.uncommittedChanges;
|
|
851
1504
|
const keys = ["staged", "modified", "untracked", "deleted", "renamed"];
|
|
@@ -856,8 +1509,23 @@ function countUncommittedChanges(status) {
|
|
|
856
1509
|
function isGitStatusDirty(status) {
|
|
857
1510
|
if (typeof status?.isDirty === "boolean") return status.isDirty;
|
|
858
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;
|
|
859
1513
|
return countUncommittedChanges(status) > 0;
|
|
860
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
|
+
}
|
|
861
1529
|
function readRelatedRepos(node) {
|
|
862
1530
|
const raw = Array.isArray(node.relatedRepos) ? node.relatedRepos : Array.isArray(node.policy?.relatedRepos) ? node.policy.relatedRepos : [];
|
|
863
1531
|
return raw.map((entry) => ({
|
|
@@ -872,6 +1540,10 @@ function summarizeRelatedRepoStatus(repo, status) {
|
|
|
872
1540
|
workspace: repo.workspace,
|
|
873
1541
|
isGitRepo: status?.isGitRepo === true,
|
|
874
1542
|
branch: status?.branch ?? null,
|
|
1543
|
+
upstream: status?.upstream ?? null,
|
|
1544
|
+
upstreamStatus: typeof status?.upstreamStatus === "string" ? status.upstreamStatus : status?.upstream ? "unchecked" : "no_upstream",
|
|
1545
|
+
upstreamFetchedAt: Number.isFinite(Number(status?.upstreamFetchedAt)) ? Number(status.upstreamFetchedAt) : null,
|
|
1546
|
+
upstreamFetchError: typeof status?.upstreamFetchError === "string" ? status.upstreamFetchError : null,
|
|
875
1547
|
ahead: Number.isFinite(Number(status?.ahead)) ? Number(status.ahead) : 0,
|
|
876
1548
|
behind: Number.isFinite(Number(status?.behind)) ? Number(status.behind) : 0,
|
|
877
1549
|
dirty,
|
|
@@ -888,7 +1560,7 @@ async function collectRelatedRepoStatuses(ctx, node) {
|
|
|
888
1560
|
const results = [];
|
|
889
1561
|
for (const repo of relatedRepos) {
|
|
890
1562
|
try {
|
|
891
|
-
const statusResult = !isLocalTransport(ctx.transport) && node.daemonId ? await ctx.transport.gitStatus(node.daemonId, repo.workspace, false) : await commandForNode(ctx, node, "git_status", { workspace: repo.workspace });
|
|
1563
|
+
const statusResult = !isLocalTransport(ctx.transport) && node.daemonId ? await ctx.transport.gitStatus(node.daemonId, repo.workspace, false, true) : await commandForNode(ctx, node, "git_status", { workspace: repo.workspace, refreshUpstream: true });
|
|
892
1564
|
const status = extractGitStatus(statusResult);
|
|
893
1565
|
results.push(summarizeRelatedRepoStatus(repo, status));
|
|
894
1566
|
} catch (e) {
|
|
@@ -912,6 +1584,16 @@ function missingProviderPriorityMessage(nodeId) {
|
|
|
912
1584
|
return `Node '${nodeId}' has no providerPriority policy; pass type explicitly or configure node.policy.providerPriority`;
|
|
913
1585
|
}
|
|
914
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
|
+
}
|
|
915
1597
|
const providerPriority = readProviderPriority(node.policy);
|
|
916
1598
|
if (providerPriority.length) {
|
|
917
1599
|
return {
|
|
@@ -926,6 +1608,33 @@ function getNodeLaunchReadiness(node) {
|
|
|
926
1608
|
launchBlockedMessage: missingProviderPriorityMessage(node.id)
|
|
927
1609
|
};
|
|
928
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
|
+
}
|
|
929
1638
|
function readNumeric(value, fallback = 0) {
|
|
930
1639
|
const parsed = Number(value);
|
|
931
1640
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -936,11 +1645,13 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
|
|
|
936
1645
|
const ahead = readNumeric(status?.ahead);
|
|
937
1646
|
const behind = readNumeric(status?.behind);
|
|
938
1647
|
const upstream = readString(status?.upstream) ?? null;
|
|
1648
|
+
const upstreamStatus = readString(status?.upstreamStatus) ?? (upstream ? "unchecked" : "no_upstream");
|
|
939
1649
|
const hasConflicts = status?.hasConflicts === true || Array.isArray(status?.conflictFiles) && status.conflictFiles.length > 0;
|
|
940
1650
|
const base = {
|
|
941
1651
|
defaultBranch,
|
|
942
1652
|
branch,
|
|
943
1653
|
upstream,
|
|
1654
|
+
upstreamStatus,
|
|
944
1655
|
ahead,
|
|
945
1656
|
behind,
|
|
946
1657
|
isWorktree: node.isLocalWorktree === true,
|
|
@@ -974,6 +1685,15 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
|
|
|
974
1685
|
};
|
|
975
1686
|
}
|
|
976
1687
|
if (branch === defaultBranch) {
|
|
1688
|
+
if (upstream && upstreamStatus !== "fresh") {
|
|
1689
|
+
return {
|
|
1690
|
+
...base,
|
|
1691
|
+
status: "blocked_review",
|
|
1692
|
+
needsConvergence: true,
|
|
1693
|
+
reason: "default_branch_upstream_unverified",
|
|
1694
|
+
nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${node.id}'.`
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
977
1697
|
if (ahead > 0 || behind > 0) {
|
|
978
1698
|
return {
|
|
979
1699
|
...base,
|
|
@@ -1000,6 +1720,15 @@ function buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges) {
|
|
|
1000
1720
|
nextStep: `Run mesh_refine_node(node_id: "${node.id}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`
|
|
1001
1721
|
};
|
|
1002
1722
|
}
|
|
1723
|
+
if (upstream && upstreamStatus !== "fresh") {
|
|
1724
|
+
return {
|
|
1725
|
+
...base,
|
|
1726
|
+
status: "blocked_review",
|
|
1727
|
+
needsConvergence: true,
|
|
1728
|
+
reason: "feature_branch_upstream_unverified",
|
|
1729
|
+
nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1003
1732
|
if (!upstream || ahead > 0 || behind > 0) {
|
|
1004
1733
|
return {
|
|
1005
1734
|
...base,
|
|
@@ -1041,7 +1770,91 @@ async function commandForNode(ctx, node, command, args = {}) {
|
|
|
1041
1770
|
if (isLocalTransport(ctx.transport)) {
|
|
1042
1771
|
return ctx.transport.command(command, args);
|
|
1043
1772
|
}
|
|
1044
|
-
|
|
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})`);
|
|
1775
|
+
}
|
|
1776
|
+
function normalizePendingMeshCoordinatorEvents(value) {
|
|
1777
|
+
const payload = unwrapCommandPayload(value);
|
|
1778
|
+
const events = Array.isArray(payload?.events) ? payload.events : Array.isArray(value?.events) ? value.events : [];
|
|
1779
|
+
return events.filter((event) => event && typeof event === "object");
|
|
1780
|
+
}
|
|
1781
|
+
function buildMeshForwardPayloadFromPendingEvent(event) {
|
|
1782
|
+
const metadataEvent = event?.metadataEvent && typeof event.metadataEvent === "object" ? event.metadataEvent : {};
|
|
1783
|
+
return {
|
|
1784
|
+
event: readString(event?.event),
|
|
1785
|
+
meshId: readString(event?.meshId),
|
|
1786
|
+
nodeId: readString(event?.nodeId) || readString(metadataEvent.meshNodeId),
|
|
1787
|
+
workspace: readString(event?.workspace) || readString(metadataEvent.workspace),
|
|
1788
|
+
targetSessionId: readString(metadataEvent.targetSessionId) || readString(metadataEvent.sessionId) || readString(metadataEvent.instanceId),
|
|
1789
|
+
providerType: readString(metadataEvent.providerType),
|
|
1790
|
+
providerSessionId: readString(metadataEvent.providerSessionId),
|
|
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 } : {},
|
|
1800
|
+
...metadataEvent.intentional === true ? { intentional: true } : {},
|
|
1801
|
+
...metadataEvent.intentionalStop === true ? { intentionalStop: true } : {},
|
|
1802
|
+
...metadataEvent.operatorCleanup === true ? { operatorCleanup: true } : {},
|
|
1803
|
+
...readString(metadataEvent.reason) ? { reason: readString(metadataEvent.reason) } : {},
|
|
1804
|
+
...readString(metadataEvent.stopReason) ? { stopReason: readString(metadataEvent.stopReason) } : {},
|
|
1805
|
+
...readString(metadataEvent.cleanupReason) ? { cleanupReason: readString(metadataEvent.cleanupReason) } : {},
|
|
1806
|
+
...readString(metadataEvent.source) ? { source: readString(metadataEvent.source) } : {}
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
async function drainCoordinatorPendingEvents(ctx, opts) {
|
|
1810
|
+
const requestedNodeIds = opts?.nodeIds?.length ? new Set(opts.nodeIds) : null;
|
|
1811
|
+
const matchesCurrentMesh = (event) => readString(event?.meshId) === ctx.mesh.id;
|
|
1812
|
+
if (ctx.transport instanceof IpcTransport) {
|
|
1813
|
+
const surfacedEvents = [];
|
|
1814
|
+
const coordinatorDaemonId = readString(ctx.localDaemonId);
|
|
1815
|
+
const pendingEventArgs = {
|
|
1816
|
+
meshId: ctx.mesh.id,
|
|
1817
|
+
...coordinatorDaemonId ? { coordinatorDaemonId } : {}
|
|
1818
|
+
};
|
|
1819
|
+
try {
|
|
1820
|
+
surfacedEvents.push(
|
|
1821
|
+
...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", pendingEventArgs)).filter(matchesCurrentMesh)
|
|
1822
|
+
);
|
|
1823
|
+
surfacedEvents.forEach(rememberMeshSessionProviderMetadataFromEvent);
|
|
1824
|
+
} catch {
|
|
1825
|
+
}
|
|
1826
|
+
for (const node of ctx.mesh.nodes) {
|
|
1827
|
+
if (!node.daemonId || isLocalControlPlaneNode(ctx, node)) continue;
|
|
1828
|
+
if (requestedNodeIds && !requestedNodeIds.has(node.id)) continue;
|
|
1829
|
+
try {
|
|
1830
|
+
const remoteEvents = normalizePendingMeshCoordinatorEvents(
|
|
1831
|
+
await ctx.transport.meshCommand(node.daemonId, "get_pending_mesh_events", pendingEventArgs)
|
|
1832
|
+
).filter(matchesCurrentMesh);
|
|
1833
|
+
if (remoteEvents.length === 0) continue;
|
|
1834
|
+
for (const event of remoteEvents) {
|
|
1835
|
+
const payload = buildMeshForwardPayloadFromPendingEvent(event);
|
|
1836
|
+
if (!payload.event || !payload.meshId) continue;
|
|
1837
|
+
await ctx.transport.command("mesh_forward_event", payload);
|
|
1838
|
+
rememberMeshSessionProviderMetadataFromEvent({ ...event, metadataEvent: payload });
|
|
1839
|
+
}
|
|
1840
|
+
} catch {
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
try {
|
|
1844
|
+
surfacedEvents.push(
|
|
1845
|
+
...normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", pendingEventArgs)).filter(matchesCurrentMesh)
|
|
1846
|
+
);
|
|
1847
|
+
surfacedEvents.forEach(rememberMeshSessionProviderMetadataFromEvent);
|
|
1848
|
+
} catch {
|
|
1849
|
+
}
|
|
1850
|
+
return surfacedEvents;
|
|
1851
|
+
}
|
|
1852
|
+
if (isLocalTransport(ctx.transport)) {
|
|
1853
|
+
const events = (0, import_daemon_core.drainPendingMeshCoordinatorEvents)(ctx.mesh.id, ctx.localDaemonId).filter(matchesCurrentMesh);
|
|
1854
|
+
events.forEach(rememberMeshSessionProviderMetadataFromEvent);
|
|
1855
|
+
return events;
|
|
1856
|
+
}
|
|
1857
|
+
return [];
|
|
1045
1858
|
}
|
|
1046
1859
|
function isP2pTransportUnavailableError(error) {
|
|
1047
1860
|
return (0, import_daemon_core.isP2pRelayTransportFailure)(error);
|
|
@@ -1056,11 +1869,12 @@ function buildRemoveNodeArgs(ctx, nodeId, sessionCleanupMode) {
|
|
|
1056
1869
|
}
|
|
1057
1870
|
var MESH_STATUS_TOOL = {
|
|
1058
1871
|
name: "mesh_status",
|
|
1059
|
-
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.",
|
|
1060
1873
|
inputSchema: {
|
|
1061
1874
|
type: "object",
|
|
1062
1875
|
properties: {
|
|
1063
|
-
_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." }
|
|
1064
1878
|
}
|
|
1065
1879
|
}
|
|
1066
1880
|
};
|
|
@@ -1080,14 +1894,18 @@ var MESH_ENQUEUE_TASK_TOOL = {
|
|
|
1080
1894
|
inputSchema: {
|
|
1081
1895
|
type: "object",
|
|
1082
1896
|
properties: {
|
|
1083
|
-
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." }
|
|
1084
1902
|
},
|
|
1085
1903
|
required: ["message"]
|
|
1086
1904
|
}
|
|
1087
1905
|
};
|
|
1088
1906
|
var MESH_VIEW_QUEUE_TOOL = {
|
|
1089
1907
|
name: "mesh_view_queue",
|
|
1090
|
-
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.",
|
|
1091
1909
|
inputSchema: {
|
|
1092
1910
|
type: "object",
|
|
1093
1911
|
properties: {
|
|
@@ -1140,7 +1958,9 @@ var MESH_SEND_TASK_TOOL = {
|
|
|
1140
1958
|
properties: {
|
|
1141
1959
|
node_id: { type: "string", description: "Target node ID (from mesh_list_nodes)." },
|
|
1142
1960
|
session_id: { type: "string", description: "Agent session ID on the target node." },
|
|
1143
|
-
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." }
|
|
1144
1964
|
},
|
|
1145
1965
|
required: ["node_id", "session_id", "message"]
|
|
1146
1966
|
}
|
|
@@ -1198,6 +2018,21 @@ var MESH_GIT_STATUS_TOOL = {
|
|
|
1198
2018
|
required: ["node_id"]
|
|
1199
2019
|
}
|
|
1200
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
|
+
};
|
|
1201
2036
|
var MESH_CHECKPOINT_TOOL = {
|
|
1202
2037
|
name: "mesh_checkpoint",
|
|
1203
2038
|
description: "Create a git checkpoint (commit) on a mesh node workspace.",
|
|
@@ -1274,38 +2109,75 @@ var MESH_CLEANUP_SESSIONS_TOOL = {
|
|
|
1274
2109
|
required: ["node_id", "mode"]
|
|
1275
2110
|
}
|
|
1276
2111
|
};
|
|
1277
|
-
var MESH_TASK_HISTORY_TOOL = {
|
|
1278
|
-
name: "mesh_task_history",
|
|
1279
|
-
description: "Read the task ledger for this mesh \u2014 dispatched tasks, completions, failures, checkpoints, and node lifecycle events. Use to understand what has been done before deciding next steps, to detect repeated failures, and to inform recovery decisions.",
|
|
2112
|
+
var MESH_TASK_HISTORY_TOOL = {
|
|
2113
|
+
name: "mesh_task_history",
|
|
2114
|
+
description: "Read the task ledger for this mesh \u2014 dispatched tasks, completions, failures, checkpoints, and node lifecycle events. Use to understand what has been done before deciding next steps, to detect repeated failures, and to inform recovery decisions.",
|
|
2115
|
+
inputSchema: {
|
|
2116
|
+
type: "object",
|
|
2117
|
+
properties: {
|
|
2118
|
+
tail: { type: "number", description: "Number of recent entries to return (default: 20)." },
|
|
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." }
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
var MESH_RECONCILE_LEDGER_TOOL = {
|
|
2124
|
+
name: "mesh_reconcile_ledger",
|
|
2125
|
+
description: "Reconcile daemon-local mesh ledgers by querying bounded ledger slices over P2P/DataChannel and importing missing entries into the coordinator local JSONL ledger. Cloud/D1 is not used as a ledger source of truth.",
|
|
2126
|
+
inputSchema: {
|
|
2127
|
+
type: "object",
|
|
2128
|
+
properties: {
|
|
2129
|
+
node_ids: { type: "array", items: { type: "string" }, description: "Optional node IDs to query. Defaults to all mesh nodes." },
|
|
2130
|
+
limit: { type: "number", description: "Bounded slice size per node. Defaults to 100 and is clamped by daemon-core." },
|
|
2131
|
+
after_id: { type: "string", description: "Optional cursor entry ID; remote slices return entries strictly after this ID when present." },
|
|
2132
|
+
since: { type: "string", description: "Optional ISO timestamp lower bound for queried entries." },
|
|
2133
|
+
import_entries: { type: "boolean", description: "When false, query and report evidence without importing remote entries. Defaults true." }
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
var MESH_REFINE_NODE_TOOL = {
|
|
2138
|
+
name: "mesh_refine_node",
|
|
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.",
|
|
2140
|
+
inputSchema: {
|
|
2141
|
+
type: "object",
|
|
2142
|
+
properties: {
|
|
2143
|
+
node_id: { type: "string", description: "Node ID of the completed worktree node to refine and merge." }
|
|
2144
|
+
},
|
|
2145
|
+
required: ["node_id"]
|
|
2146
|
+
}
|
|
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.",
|
|
1280
2156
|
inputSchema: {
|
|
1281
2157
|
type: "object",
|
|
1282
2158
|
properties: {
|
|
1283
|
-
|
|
1284
|
-
|
|
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." }
|
|
1285
2161
|
}
|
|
1286
2162
|
}
|
|
1287
2163
|
};
|
|
1288
|
-
var
|
|
1289
|
-
name: "
|
|
1290
|
-
description: "
|
|
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.",
|
|
1291
2167
|
inputSchema: {
|
|
1292
2168
|
type: "object",
|
|
1293
2169
|
properties: {
|
|
1294
|
-
|
|
1295
|
-
limit: { type: "number", description: "Bounded slice size per node. Defaults to 100 and is clamped by daemon-core." },
|
|
1296
|
-
after_id: { type: "string", description: "Optional cursor entry ID; remote slices return entries strictly after this ID when present." },
|
|
1297
|
-
since: { type: "string", description: "Optional ISO timestamp lower bound for queried entries." },
|
|
1298
|
-
import_entries: { type: "boolean", description: "When false, query and report evidence without importing remote entries. Defaults true." }
|
|
2170
|
+
node_id: { type: "string", description: "Optional node/workspace used for suggestions. Defaults to the first mesh node." }
|
|
1299
2171
|
}
|
|
1300
2172
|
}
|
|
1301
2173
|
};
|
|
1302
|
-
var
|
|
1303
|
-
name: "
|
|
1304
|
-
description: "
|
|
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.",
|
|
1305
2177
|
inputSchema: {
|
|
1306
2178
|
type: "object",
|
|
1307
2179
|
properties: {
|
|
1308
|
-
node_id: { type: "string", description: "Node ID of the
|
|
2180
|
+
node_id: { type: "string", description: "Node ID of the worktree node to plan." }
|
|
1309
2181
|
},
|
|
1310
2182
|
required: ["node_id"]
|
|
1311
2183
|
}
|
|
@@ -1322,33 +2194,41 @@ var ALL_MESH_TOOLS = [
|
|
|
1322
2194
|
MESH_READ_DEBUG_TOOL,
|
|
1323
2195
|
MESH_LAUNCH_SESSION_TOOL,
|
|
1324
2196
|
MESH_GIT_STATUS_TOOL,
|
|
2197
|
+
MESH_FAST_FORWARD_NODE_TOOL,
|
|
1325
2198
|
MESH_CHECKPOINT_TOOL,
|
|
1326
2199
|
MESH_APPROVE_TOOL,
|
|
1327
2200
|
MESH_CLONE_NODE_TOOL,
|
|
1328
2201
|
MESH_REMOVE_NODE_TOOL,
|
|
1329
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,
|
|
1330
2207
|
MESH_CLEANUP_SESSIONS_TOOL,
|
|
1331
2208
|
MESH_TASK_HISTORY_TOOL,
|
|
1332
2209
|
MESH_RECONCILE_LEDGER_TOOL
|
|
1333
2210
|
];
|
|
1334
|
-
async function meshStatus(ctx) {
|
|
2211
|
+
async function meshStatus(ctx, args = {}) {
|
|
1335
2212
|
await refreshMeshFromDaemon(ctx);
|
|
1336
2213
|
const { mesh, transport } = ctx;
|
|
1337
|
-
|
|
1338
|
-
const
|
|
1339
|
-
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) => {
|
|
1340
2216
|
const entry = {
|
|
1341
2217
|
nodeId: node.id,
|
|
1342
2218
|
workspace: node.workspace,
|
|
2219
|
+
machine: buildNodeMachineIdentity(ctx, node),
|
|
2220
|
+
daemonId: readNodeDaemonId(node),
|
|
2221
|
+
machineId: readNodeMachineId(node),
|
|
1343
2222
|
...getNodeLaunchReadiness(node)
|
|
1344
2223
|
};
|
|
1345
2224
|
try {
|
|
1346
2225
|
if (!isLocalTransport(transport) && node.daemonId) {
|
|
1347
|
-
const result = await transport.gitStatus(node.daemonId, node.workspace, false);
|
|
2226
|
+
const result = await transport.gitStatus(node.daemonId, node.workspace, false, true);
|
|
1348
2227
|
const status = extractGitStatus(result);
|
|
1349
2228
|
const uncommittedChanges = countUncommittedChanges(status);
|
|
1350
2229
|
const dirty = isGitStatusDirty(status);
|
|
1351
2230
|
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
2231
|
+
assignFullGitSnapshot(entry, status);
|
|
1352
2232
|
entry.branch = status?.branch;
|
|
1353
2233
|
entry.isDirty = dirty;
|
|
1354
2234
|
entry.uncommittedChanges = uncommittedChanges;
|
|
@@ -1362,6 +2242,7 @@ async function meshStatus(ctx) {
|
|
|
1362
2242
|
const autoDiscover = node.policy?.autoDiscoverSubmodules !== false;
|
|
1363
2243
|
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
1364
2244
|
workspace: node.workspace,
|
|
2245
|
+
refreshUpstream: true,
|
|
1365
2246
|
includeSubmodules: autoDiscover,
|
|
1366
2247
|
submoduleIgnorePaths: node.policy?.submoduleIgnorePaths || void 0
|
|
1367
2248
|
});
|
|
@@ -1369,6 +2250,7 @@ async function meshStatus(ctx) {
|
|
|
1369
2250
|
const uncommittedChanges = countUncommittedChanges(status);
|
|
1370
2251
|
const dirty = isGitStatusDirty(status);
|
|
1371
2252
|
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
2253
|
+
assignFullGitSnapshot(entry, status);
|
|
1372
2254
|
entry.branch = status?.branch;
|
|
1373
2255
|
entry.isDirty = dirty;
|
|
1374
2256
|
entry.uncommittedChanges = uncommittedChanges;
|
|
@@ -1404,7 +2286,7 @@ async function meshStatus(ctx) {
|
|
|
1404
2286
|
if (recoveryContext.consecutiveNodeFailures > 0) {
|
|
1405
2287
|
entry.recoveryHints = {
|
|
1406
2288
|
consecutiveFailures: recoveryContext.consecutiveNodeFailures,
|
|
1407
|
-
lastTaskMessage: recoveryContext.lastTaskMessage,
|
|
2289
|
+
lastTaskMessage: typeof recoveryContext.lastTaskMessage === "string" ? recoveryContext.lastTaskMessage.slice(0, 100) + (recoveryContext.lastTaskMessage.length > 100 ? "\u2026" : "") : recoveryContext.lastTaskMessage,
|
|
1408
2290
|
advice: recoveryContext.advice,
|
|
1409
2291
|
retryRecommended: recoveryContext.retryRecommended
|
|
1410
2292
|
};
|
|
@@ -1444,7 +2326,55 @@ async function meshStatus(ctx) {
|
|
|
1444
2326
|
}
|
|
1445
2327
|
const relatedRepos = await collectRelatedRepoStatuses(ctx, node);
|
|
1446
2328
|
if (relatedRepos.length) entry.relatedRepos = relatedRepos;
|
|
1447
|
-
|
|
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
|
+
}
|
|
1448
2378
|
}
|
|
1449
2379
|
const response = {
|
|
1450
2380
|
meshId: mesh.id,
|
|
@@ -1452,20 +2382,43 @@ async function meshStatus(ctx) {
|
|
|
1452
2382
|
repoIdentity: mesh.repoIdentity,
|
|
1453
2383
|
policy: mesh.policy,
|
|
1454
2384
|
refreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2385
|
+
sourceOfTruth: {
|
|
2386
|
+
membership: "coordinator_daemon_live_mesh",
|
|
2387
|
+
currentStatus: "live_git_and_session_probes",
|
|
2388
|
+
activeWork: "mesh_queue_file_and_local_ledger",
|
|
2389
|
+
historicalEvidenceOnly: ["recoveryHints", "ledgerSummary"]
|
|
2390
|
+
},
|
|
1455
2391
|
nodes: results,
|
|
1456
|
-
|
|
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
|
+
} : {}
|
|
1457
2408
|
};
|
|
1458
2409
|
try {
|
|
1459
2410
|
response.ledgerSummary = ledgerSummary;
|
|
1460
2411
|
} catch {
|
|
1461
2412
|
}
|
|
1462
2413
|
try {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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;
|
|
1469
2422
|
}
|
|
1470
2423
|
if (pendingEvents.length > 0) {
|
|
1471
2424
|
response.pendingCoordinatorEvents = pendingEvents;
|
|
@@ -1476,11 +2429,21 @@ async function meshStatus(ctx) {
|
|
|
1476
2429
|
}
|
|
1477
2430
|
async function meshTaskHistory(ctx, args) {
|
|
1478
2431
|
const { mesh } = ctx;
|
|
2432
|
+
const pendingEvents = await drainCoordinatorPendingEvents(ctx);
|
|
1479
2433
|
const tail = typeof args.tail === "number" && args.tail > 0 ? args.tail : 20;
|
|
1480
2434
|
const kind = typeof args.kind === "string" && args.kind.trim() ? [args.kind.trim()] : void 0;
|
|
1481
|
-
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
|
+
}));
|
|
1482
2440
|
const summary = (0, import_daemon_core.getLedgerSummary)(mesh.id);
|
|
1483
|
-
return JSON.stringify({
|
|
2441
|
+
return JSON.stringify({
|
|
2442
|
+
meshId: mesh.id,
|
|
2443
|
+
entries,
|
|
2444
|
+
summary,
|
|
2445
|
+
...pendingEvents.length > 0 ? { pendingCoordinatorEvents: pendingEvents } : {}
|
|
2446
|
+
}, null, 2);
|
|
1484
2447
|
}
|
|
1485
2448
|
async function meshReconcileLedger(ctx, args) {
|
|
1486
2449
|
await refreshMeshFromDaemon(ctx);
|
|
@@ -1570,6 +2533,9 @@ async function meshListNodes(ctx) {
|
|
|
1570
2533
|
nodeId: n.id,
|
|
1571
2534
|
workspace: n.workspace,
|
|
1572
2535
|
repoRoot: n.repoRoot,
|
|
2536
|
+
daemonId: readNodeDaemonId(n),
|
|
2537
|
+
machineId: readNodeMachineId(n),
|
|
2538
|
+
machine: buildNodeMachineIdentity(ctx, n),
|
|
1573
2539
|
isLocalWorktree: n.isLocalWorktree,
|
|
1574
2540
|
policy: n.policy,
|
|
1575
2541
|
relatedRepos: readRelatedRepos(n),
|
|
@@ -1579,57 +2545,135 @@ async function meshListNodes(ctx) {
|
|
|
1579
2545
|
}, null, 2);
|
|
1580
2546
|
}
|
|
1581
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);
|
|
1582
2550
|
try {
|
|
1583
|
-
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 });
|
|
1584
2552
|
if (isLocalTransport(ctx.transport) && !(ctx.transport instanceof IpcTransport)) {
|
|
1585
|
-
|
|
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)
|
|
1586
2563
|
});
|
|
1587
|
-
return JSON.stringify({ success: true, taskId: task.id, status: task.status });
|
|
1588
2564
|
}
|
|
1589
2565
|
if (ctx.transport instanceof IpcTransport) {
|
|
1590
|
-
|
|
1591
|
-
});
|
|
2566
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx);
|
|
1592
2567
|
const dispatchPromises = [];
|
|
1593
2568
|
for (const node of ctx.mesh.nodes) {
|
|
1594
2569
|
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
1595
2570
|
if (isLocalNode || !node.daemonId) continue;
|
|
2571
|
+
if (!(0, import_daemon_core.nodeSatisfiesRequiredTags)(requiredTags, (0, import_daemon_core.buildMeshNodeCapabilityTags)(node))) continue;
|
|
1596
2572
|
dispatchPromises.push(
|
|
1597
2573
|
ipcDispatchToRemoteAgent(ctx, node, { message: args.message }).then((result) => {
|
|
1598
2574
|
if (result.success) {
|
|
1599
2575
|
try {
|
|
2576
|
+
const providerType = result.providerType;
|
|
2577
|
+
const descriptor = summarizeTaskMessage(args.message);
|
|
1600
2578
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
1601
2579
|
kind: "task_dispatched",
|
|
1602
2580
|
nodeId: node.id,
|
|
1603
2581
|
sessionId: result.sessionId,
|
|
1604
|
-
|
|
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
|
+
}
|
|
1605
2594
|
});
|
|
1606
2595
|
} catch {
|
|
1607
2596
|
}
|
|
1608
2597
|
}
|
|
1609
|
-
}).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
|
+
}
|
|
1610
2613
|
})
|
|
1611
2614
|
);
|
|
1612
2615
|
}
|
|
1613
2616
|
Promise.all(dispatchPromises).catch(() => {
|
|
1614
2617
|
});
|
|
1615
|
-
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
|
+
});
|
|
1616
2628
|
}
|
|
1617
|
-
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 });
|
|
1618
2630
|
} catch (e) {
|
|
1619
|
-
|
|
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 });
|
|
1620
2636
|
}
|
|
1621
2637
|
}
|
|
1622
2638
|
async function meshViewQueue(ctx, args) {
|
|
1623
2639
|
try {
|
|
2640
|
+
await refreshMeshFromDaemon(ctx);
|
|
1624
2641
|
const statusFilter = sanitizeQueueStatusFilter(args.status);
|
|
1625
2642
|
const view = normalizeQueueViewMode(args.view);
|
|
1626
|
-
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));
|
|
1627
2644
|
const queue = filterQueueForView(fullQueue, view, statusFilter);
|
|
1628
2645
|
const summary = buildQueueStatusSummary(fullQueue);
|
|
1629
2646
|
const visibleSummary = buildQueueStatusSummary(queue);
|
|
1630
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 MeshRuntimeStore records (may be empty). buildMeshActiveWork uses them for local
|
|
2663
|
+
// dispatches and falls through to ledger scan for remote P2P dispatches not in MeshRuntimeStore.
|
|
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
|
+
}));
|
|
1631
2674
|
const staleAssignedTasks = maintenance.staleAssignedTasks || [];
|
|
1632
2675
|
const requestedHistoricalRows = queue.some((task) => HISTORICAL_QUEUE_STATUSES.has(String(task?.status || "")));
|
|
2676
|
+
const pollingGuidance = buildActiveWorkPollingGuidance(activeWorkEvidence.summary);
|
|
1633
2677
|
return JSON.stringify({
|
|
1634
2678
|
success: true,
|
|
1635
2679
|
sourceOfTruth: {
|
|
@@ -1644,21 +2688,29 @@ async function meshViewQueue(ctx, args) {
|
|
|
1644
2688
|
filtered: Boolean(statusFilter?.length) || view !== "all"
|
|
1645
2689
|
},
|
|
1646
2690
|
queue,
|
|
1647
|
-
|
|
1648
|
-
|
|
2691
|
+
activeWork: activeWorkEvidence.activeWork,
|
|
2692
|
+
staleDirectWork: activeWorkEvidence.staleDirectWork,
|
|
2693
|
+
activeWorkSummary: activeWorkEvidence.summary,
|
|
2694
|
+
...pollingGuidance ? { pollingGuidance } : {},
|
|
1649
2695
|
summary,
|
|
2696
|
+
visibleSummary,
|
|
1650
2697
|
activeCounts: summary.activeCounts,
|
|
1651
2698
|
historicalCounts: summary.historicalCounts,
|
|
1652
|
-
activeCount: summary.activeCount,
|
|
1653
|
-
historicalCount: summary.historicalCount,
|
|
1654
2699
|
visibleActiveCounts: visibleSummary.activeCounts,
|
|
1655
2700
|
visibleHistoricalCounts: visibleSummary.historicalCounts,
|
|
2701
|
+
activeCount: summary.activeCount,
|
|
2702
|
+
historicalCount: summary.historicalCount,
|
|
1656
2703
|
visibleActiveCount: visibleSummary.activeCount,
|
|
1657
2704
|
visibleHistoricalCount: visibleSummary.historicalCount,
|
|
1658
2705
|
staleAssignedTasks,
|
|
1659
2706
|
staleAssignedCount: maintenance.staleAssignedCount,
|
|
1660
2707
|
queueMaintenance: maintenance,
|
|
1661
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
|
+
} : {},
|
|
1662
2714
|
...view === "active" || statusFilter?.some((status) => ACTIVE_QUEUE_STATUSES.has(status)) ? {
|
|
1663
2715
|
activeQueue: queue.filter((task) => ACTIVE_QUEUE_STATUSES.has(String(task?.status || "")))
|
|
1664
2716
|
} : {},
|
|
@@ -1678,6 +2730,10 @@ async function meshQueueCancel(ctx, args) {
|
|
|
1678
2730
|
if (!taskId) return JSON.stringify({ success: false, error: "task_id required" });
|
|
1679
2731
|
const task = (0, import_daemon_core.cancelTask)(ctx.mesh.id, taskId, { reason: args.reason });
|
|
1680
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
|
+
}
|
|
1681
2737
|
return JSON.stringify({ success: true, task }, null, 2);
|
|
1682
2738
|
} catch (e) {
|
|
1683
2739
|
return JSON.stringify({ success: false, error: e.message });
|
|
@@ -1708,10 +2764,60 @@ async function meshQueueRequeue(ctx, args) {
|
|
|
1708
2764
|
}
|
|
1709
2765
|
}
|
|
1710
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;
|
|
1711
2780
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
1712
2781
|
if (node.policy?.readOnly) {
|
|
1713
2782
|
return JSON.stringify({ error: `Node '${args.node_id}' is read-only` });
|
|
1714
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
|
+
}
|
|
1715
2821
|
const duplicate = hasRecentDuplicateDispatch(ctx, args);
|
|
1716
2822
|
if (duplicate.duplicate) {
|
|
1717
2823
|
return JSON.stringify({
|
|
@@ -1735,47 +2841,191 @@ async function meshSendTask(ctx, args) {
|
|
|
1735
2841
|
const res = await ctx.transport.meshEnqueueTask(node.daemonId, {
|
|
1736
2842
|
meshId: ctx.mesh.id,
|
|
1737
2843
|
message: args.message,
|
|
1738
|
-
targetNodeId: args.node_id
|
|
2844
|
+
targetNodeId: args.node_id,
|
|
2845
|
+
...taskMode ? { taskMode } : {}
|
|
1739
2846
|
});
|
|
1740
2847
|
return JSON.stringify(res);
|
|
1741
2848
|
}
|
|
1742
2849
|
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
1743
2850
|
if (ctx.transport instanceof IpcTransport && node.daemonId && !isLocalNode) {
|
|
1744
|
-
const cached =
|
|
2851
|
+
const cached = getSessionMetadata(meshSessionCacheKey(args.node_id, args.session_id || ""));
|
|
2852
|
+
const taskId = (0, import_node_crypto.randomUUID)();
|
|
1745
2853
|
const result2 = await ipcDispatchToRemoteAgent(ctx, node, {
|
|
1746
2854
|
session_id: args.session_id,
|
|
1747
2855
|
message: args.message,
|
|
1748
|
-
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
|
+
}
|
|
1749
2863
|
});
|
|
1750
2864
|
if (result2.success) {
|
|
1751
2865
|
const dispatchedSessionId = args.session_id || result2.sessionId;
|
|
2866
|
+
const dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1752
2867
|
try {
|
|
2868
|
+
const providerType = result2.providerType || cached?.providerType;
|
|
1753
2869
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
1754
2870
|
kind: "task_dispatched",
|
|
1755
2871
|
nodeId: args.node_id,
|
|
1756
2872
|
sessionId: dispatchedSessionId,
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
|
1762
2890
|
});
|
|
1763
2891
|
} catch {
|
|
1764
2892
|
}
|
|
1765
2893
|
}
|
|
1766
|
-
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
|
+
});
|
|
1767
2903
|
}
|
|
1768
2904
|
if (args.session_id && isLocalTransport(ctx.transport)) {
|
|
1769
|
-
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
|
+
if (explicitTargetSession && !isIdleSessionRecord(explicitTargetSession) && !isTerminalSessionRecord(explicitTargetSession)) {
|
|
2980
|
+
const sessionStatus = typeof explicitTargetSession?.status === "string" ? explicitTargetSession.status : "unknown";
|
|
2981
|
+
const { createSessionDelivery: createDelivery, resolveDeliveryDecision } = await import("@adhdev/daemon-core");
|
|
2982
|
+
const policyResult = resolveDeliveryDecision(sessionStatus, { kind: "task" });
|
|
2983
|
+
if (policyResult.decision === "queued") {
|
|
2984
|
+
const delivery = createDelivery({
|
|
2985
|
+
meshId: ctx.mesh.id,
|
|
2986
|
+
nodeId: args.node_id,
|
|
2987
|
+
sessionId: args.session_id,
|
|
2988
|
+
providerType: resolvedProviderType,
|
|
2989
|
+
kind: "task",
|
|
2990
|
+
message: args.message,
|
|
2991
|
+
status: "queued"
|
|
2992
|
+
});
|
|
2993
|
+
return JSON.stringify({
|
|
2994
|
+
success: true,
|
|
2995
|
+
dispatched: false,
|
|
2996
|
+
decision: "queued_delivery",
|
|
2997
|
+
deliveryId: delivery.id,
|
|
2998
|
+
reason: policyResult.reason,
|
|
2999
|
+
nodeId: args.node_id,
|
|
3000
|
+
sessionId: args.session_id,
|
|
3001
|
+
sessionStatus,
|
|
3002
|
+
taskMode: taskMode || void 0,
|
|
3003
|
+
message: policyResult.message,
|
|
3004
|
+
nextAction: `Use mesh_status to watch for session idle transition, or use mesh_enqueue_task for queue-based assignment. Check deliveryId '${delivery.id}' to track queued delivery.`
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
const sessionWasIdle = explicitTargetSession ? isIdleSessionRecord(explicitTargetSession) : false;
|
|
3009
|
+
const taskId = (0, import_node_crypto.randomUUID)();
|
|
3010
|
+
const dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1770
3011
|
const dispatchResult = await commandForNode(ctx, node, "agent_command", {
|
|
1771
3012
|
targetSessionId: args.session_id,
|
|
1772
|
-
|
|
3013
|
+
agentType: resolvedProviderType,
|
|
3014
|
+
cliType: resolvedProviderType,
|
|
3015
|
+
providerType: resolvedProviderType,
|
|
1773
3016
|
action: "send_chat",
|
|
1774
|
-
message: args.message
|
|
3017
|
+
message: args.message,
|
|
3018
|
+
meshContext: {
|
|
3019
|
+
meshId: ctx.mesh.id,
|
|
3020
|
+
nodeId: args.node_id,
|
|
3021
|
+
taskId
|
|
3022
|
+
}
|
|
1775
3023
|
});
|
|
1776
3024
|
const dispatchPayload = unwrapCommandPayload(dispatchResult);
|
|
1777
3025
|
if (dispatchPayload?.success === false || dispatchResult?.success === false) {
|
|
3026
|
+
const source = dispatchPayload?.success === false ? dispatchPayload : dispatchResult;
|
|
1778
3027
|
return JSON.stringify({
|
|
3028
|
+
...source && typeof source === "object" ? source : {},
|
|
1779
3029
|
success: false,
|
|
1780
3030
|
nodeId: args.node_id,
|
|
1781
3031
|
sessionId: args.session_id,
|
|
@@ -1787,23 +3037,79 @@ async function meshSendTask(ctx, args) {
|
|
|
1787
3037
|
kind: "task_dispatched",
|
|
1788
3038
|
nodeId: args.node_id,
|
|
1789
3039
|
sessionId: args.session_id,
|
|
1790
|
-
providerType:
|
|
1791
|
-
payload:
|
|
3040
|
+
providerType: resolvedProviderType,
|
|
3041
|
+
payload: buildDirectTaskPayload(args.message, "local_direct", {
|
|
3042
|
+
taskId,
|
|
3043
|
+
taskMode,
|
|
3044
|
+
providerType: resolvedProviderType,
|
|
3045
|
+
targetSessionId: args.session_id,
|
|
3046
|
+
dispatchedToIdleSession: sessionWasIdle
|
|
3047
|
+
})
|
|
3048
|
+
});
|
|
3049
|
+
} catch {
|
|
3050
|
+
}
|
|
3051
|
+
(0, import_daemon_core.insertDirectDispatch)(ctx.mesh.id, {
|
|
3052
|
+
taskId,
|
|
3053
|
+
nodeId: args.node_id,
|
|
3054
|
+
sessionId: args.session_id,
|
|
3055
|
+
providerType: resolvedProviderType || void 0,
|
|
3056
|
+
message: args.message,
|
|
3057
|
+
taskMode: taskMode || void 0,
|
|
3058
|
+
via: "local_direct",
|
|
3059
|
+
dispatchedToIdleSession: sessionWasIdle,
|
|
3060
|
+
dispatchedAt
|
|
3061
|
+
});
|
|
3062
|
+
let deliveryId;
|
|
3063
|
+
try {
|
|
3064
|
+
const { createSessionDelivery: createDelivery } = await import("@adhdev/daemon-core");
|
|
3065
|
+
const delivery = createDelivery({
|
|
3066
|
+
meshId: ctx.mesh.id,
|
|
3067
|
+
nodeId: args.node_id,
|
|
3068
|
+
sessionId: args.session_id,
|
|
3069
|
+
providerType: resolvedProviderType || void 0,
|
|
3070
|
+
taskId,
|
|
3071
|
+
kind: "task",
|
|
3072
|
+
message: args.message,
|
|
3073
|
+
status: sessionWasIdle ? "delivered" : "delivering"
|
|
1792
3074
|
});
|
|
3075
|
+
deliveryId = delivery.id;
|
|
1793
3076
|
} catch {
|
|
1794
3077
|
}
|
|
1795
|
-
return JSON.stringify({
|
|
3078
|
+
return JSON.stringify({
|
|
3079
|
+
success: true,
|
|
3080
|
+
dispatched: true,
|
|
3081
|
+
decision: "immediate",
|
|
3082
|
+
source: "direct",
|
|
3083
|
+
taskId,
|
|
3084
|
+
deliveryId,
|
|
3085
|
+
taskMode,
|
|
3086
|
+
providerType: resolvedProviderType,
|
|
3087
|
+
nodeId: args.node_id,
|
|
3088
|
+
sessionId: args.session_id,
|
|
3089
|
+
...sessionWasIdle ? {
|
|
3090
|
+
dispatchAcknowledgementRisk: true,
|
|
3091
|
+
dispatchAcknowledgementRiskReason: "session_was_idle_at_dispatch",
|
|
3092
|
+
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.`
|
|
3093
|
+
} : {}
|
|
3094
|
+
});
|
|
1796
3095
|
}
|
|
1797
3096
|
const task = (0, import_daemon_core.enqueueTask)(ctx.mesh.id, args.message, {
|
|
1798
3097
|
targetNodeId: args.node_id,
|
|
1799
|
-
targetSessionId: args.session_id
|
|
3098
|
+
targetSessionId: args.session_id,
|
|
3099
|
+
taskMode
|
|
1800
3100
|
});
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
3101
|
+
const queueTrigger = isLocalTransport(ctx.transport) || ctx.transport instanceof IpcTransport ? await triggerMeshQueueAndReport(ctx) : void 0;
|
|
3102
|
+
const pendingEvents = isLocalTransport(ctx.transport) ? (0, import_daemon_core.drainPendingMeshCoordinatorEvents)(ctx.mesh.id, ctx.localDaemonId) : [];
|
|
3103
|
+
const result = {
|
|
3104
|
+
success: true,
|
|
3105
|
+
source: "queue",
|
|
3106
|
+
nodeId: args.node_id,
|
|
3107
|
+
taskId: task.id,
|
|
3108
|
+
status: task.status,
|
|
3109
|
+
taskMode: task.taskMode,
|
|
3110
|
+
queueTrigger,
|
|
3111
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
3112
|
+
};
|
|
1807
3113
|
if (pendingEvents.length > 0) {
|
|
1808
3114
|
result.pendingCoordinatorEvents = pendingEvents;
|
|
1809
3115
|
}
|
|
@@ -1823,8 +3129,11 @@ async function meshReadChat(ctx, args) {
|
|
|
1823
3129
|
if (!node) {
|
|
1824
3130
|
return JSON.stringify(buildMissingNodeReadChatRecovery(ctx, args), null, 2);
|
|
1825
3131
|
}
|
|
3132
|
+
if (ctx.transport instanceof IpcTransport || isLocalTransport(ctx.transport)) {
|
|
3133
|
+
await drainCoordinatorPendingEvents(ctx, { nodeIds: [args.node_id] });
|
|
3134
|
+
}
|
|
1826
3135
|
if (isLocalTransport(ctx.transport)) {
|
|
1827
|
-
const cached =
|
|
3136
|
+
const cached = resolveMeshSessionProviderMetadata(ctx, args.node_id, args.session_id);
|
|
1828
3137
|
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
1829
3138
|
const result = await commandForNode(ctx, node, "read_chat", {
|
|
1830
3139
|
sessionId: args.session_id,
|
|
@@ -1832,18 +3141,19 @@ async function meshReadChat(ctx, args) {
|
|
|
1832
3141
|
workspace: node.workspace,
|
|
1833
3142
|
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
1834
3143
|
...providerSessionId ? { providerSessionId } : {},
|
|
1835
|
-
tailLimit: args.tail ??
|
|
3144
|
+
tailLimit: args.tail ?? 3
|
|
1836
3145
|
});
|
|
1837
3146
|
const payload = annotateRapidReadChatAdvisory(unwrapCommandPayload(result), {
|
|
1838
3147
|
key: `mesh:${args.node_id}:${args.session_id}`,
|
|
1839
3148
|
toolName: "mesh_read_chat",
|
|
1840
3149
|
completionCallbackExpected: true
|
|
1841
3150
|
});
|
|
1842
|
-
|
|
3151
|
+
const useCompact = args.compact !== false;
|
|
3152
|
+
if (useCompact) {
|
|
1843
3153
|
const compactPayload = compactChatPayload(payload, {
|
|
1844
3154
|
nodeId: args.node_id,
|
|
1845
3155
|
sessionId: args.session_id,
|
|
1846
|
-
limit: args.tail ??
|
|
3156
|
+
limit: args.tail ?? 3
|
|
1847
3157
|
});
|
|
1848
3158
|
return JSON.stringify(
|
|
1849
3159
|
payload.pollingAdvisory ? { ...compactPayload, pollingAdvisory: payload.pollingAdvisory } : compactPayload,
|
|
@@ -1856,7 +3166,7 @@ async function meshReadChat(ctx, args) {
|
|
|
1856
3166
|
try {
|
|
1857
3167
|
const targetId = `${node.daemonId}:session:${args.session_id}`;
|
|
1858
3168
|
const res = await ctx.transport.readChat(targetId, {
|
|
1859
|
-
limit: args.tail ??
|
|
3169
|
+
limit: args.tail ?? 3,
|
|
1860
3170
|
sessionId: args.session_id
|
|
1861
3171
|
});
|
|
1862
3172
|
return JSON.stringify(res, null, 2);
|
|
@@ -1870,7 +3180,7 @@ async function meshReadChat(ctx, args) {
|
|
|
1870
3180
|
async function meshReadDebug(ctx, args) {
|
|
1871
3181
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
1872
3182
|
if (isLocalTransport(ctx.transport)) {
|
|
1873
|
-
const cached =
|
|
3183
|
+
const cached = resolveMeshSessionProviderMetadata(ctx, args.node_id, args.session_id);
|
|
1874
3184
|
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
1875
3185
|
const delivery = args.delivery === "inline" ? void 0 : "daemon_file";
|
|
1876
3186
|
const result = await commandForNode(ctx, node, "get_chat_debug_bundle", {
|
|
@@ -1901,6 +3211,8 @@ async function meshReadDebug(ctx, args) {
|
|
|
1901
3211
|
}
|
|
1902
3212
|
async function meshLaunchSession(ctx, args) {
|
|
1903
3213
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3214
|
+
const bootstrapBlock = getWorktreeBootstrapLaunchBlock(node);
|
|
3215
|
+
if (bootstrapBlock) return JSON.stringify(bootstrapBlock, null, 2);
|
|
1904
3216
|
if (isLocalTransport(ctx.transport)) {
|
|
1905
3217
|
let resolvedProviderType = typeof args.type === "string" && args.type.trim() ? args.type : "";
|
|
1906
3218
|
if (!resolvedProviderType) {
|
|
@@ -1925,6 +3237,10 @@ async function meshLaunchSession(ctx, args) {
|
|
|
1925
3237
|
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
1926
3238
|
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
1927
3239
|
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
3240
|
+
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
3241
|
+
if (node.daemonId && !isLocalNode && !coordinatorDaemonId) {
|
|
3242
|
+
return JSON.stringify(buildMissingCoordinatorDaemonIdFailure(ctx, node, resolvedProviderType), null, 2);
|
|
3243
|
+
}
|
|
1928
3244
|
let result;
|
|
1929
3245
|
try {
|
|
1930
3246
|
result = await commandForNode(ctx, node, "launch_cli", {
|
|
@@ -1952,7 +3268,8 @@ async function meshLaunchSession(ctx, args) {
|
|
|
1952
3268
|
if (runtimeSessionId) {
|
|
1953
3269
|
meshSessionProviderMetadata.set(meshSessionCacheKey(args.node_id, runtimeSessionId), {
|
|
1954
3270
|
providerType: resolvedProviderType,
|
|
1955
|
-
...providerSessionId ? { providerSessionId } : {}
|
|
3271
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3272
|
+
expiresAt: Date.now() + SESSION_PROVIDER_METADATA_TTL_MS
|
|
1956
3273
|
});
|
|
1957
3274
|
}
|
|
1958
3275
|
try {
|
|
@@ -1965,18 +3282,13 @@ async function meshLaunchSession(ctx, args) {
|
|
|
1965
3282
|
});
|
|
1966
3283
|
} catch {
|
|
1967
3284
|
}
|
|
1968
|
-
const
|
|
1969
|
-
if (ctx.transport instanceof IpcTransport && node.daemonId && !isLocalNode) {
|
|
1970
|
-
ctx.transport.meshCommand(node.daemonId, "trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
1971
|
-
});
|
|
1972
|
-
} else if (isLocalTransport(ctx.transport)) {
|
|
1973
|
-
ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
3285
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx, node, { localNode: isLocalNode });
|
|
1976
3286
|
return JSON.stringify({
|
|
1977
3287
|
...launchPayload,
|
|
1978
3288
|
resolvedProviderType,
|
|
1979
|
-
...providerSessionId ? { providerSessionId } : {}
|
|
3289
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3290
|
+
queueTrigger,
|
|
3291
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
1980
3292
|
}, null, 2);
|
|
1981
3293
|
} else if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
1982
3294
|
let resolvedProviderType = typeof args.type === "string" && args.type.trim() ? args.type : "";
|
|
@@ -1990,6 +3302,9 @@ async function meshLaunchSession(ctx, args) {
|
|
|
1990
3302
|
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
1991
3303
|
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
1992
3304
|
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
3305
|
+
if (!coordinatorDaemonId) {
|
|
3306
|
+
return JSON.stringify(buildMissingCoordinatorDaemonIdFailure(ctx, node, resolvedProviderType), null, 2);
|
|
3307
|
+
}
|
|
1993
3308
|
try {
|
|
1994
3309
|
const res = await ctx.transport.launch(node.daemonId, {
|
|
1995
3310
|
type: resolvedProviderType,
|
|
@@ -2028,7 +3343,7 @@ async function meshGitStatus(ctx, args) {
|
|
|
2028
3343
|
const submoduleIgnorePaths = node.policy?.submoduleIgnorePaths || [];
|
|
2029
3344
|
try {
|
|
2030
3345
|
if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2031
|
-
const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true);
|
|
3346
|
+
const result = await ctx.transport.gitStatus(node.daemonId, node.workspace, true, true);
|
|
2032
3347
|
return JSON.stringify({
|
|
2033
3348
|
nodeId: args.node_id,
|
|
2034
3349
|
workspace: node.workspace,
|
|
@@ -2040,6 +3355,7 @@ async function meshGitStatus(ctx, args) {
|
|
|
2040
3355
|
} else if (isLocalTransport(ctx.transport)) {
|
|
2041
3356
|
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
2042
3357
|
workspace: node.workspace,
|
|
3358
|
+
refreshUpstream: true,
|
|
2043
3359
|
includeSubmodules: autoDiscoverSubmodules,
|
|
2044
3360
|
submoduleIgnorePaths: submoduleIgnorePaths.length > 0 ? submoduleIgnorePaths : void 0
|
|
2045
3361
|
});
|
|
@@ -2069,6 +3385,51 @@ async function meshGitStatus(ctx, args) {
|
|
|
2069
3385
|
}, null, 2);
|
|
2070
3386
|
}
|
|
2071
3387
|
}
|
|
3388
|
+
async function meshFastForwardNode(ctx, args) {
|
|
3389
|
+
await refreshMeshFromDaemon(ctx);
|
|
3390
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3391
|
+
const submoduleIgnorePaths = node.policy?.submoduleIgnorePaths || [];
|
|
3392
|
+
if (node.policy?.readOnly) {
|
|
3393
|
+
return JSON.stringify({
|
|
3394
|
+
success: false,
|
|
3395
|
+
code: "node_read_only",
|
|
3396
|
+
nodeId: args.node_id,
|
|
3397
|
+
workspace: node.workspace,
|
|
3398
|
+
allowed: false,
|
|
3399
|
+
willRun: false,
|
|
3400
|
+
executed: false,
|
|
3401
|
+
blockingReasons: ["node_read_only"]
|
|
3402
|
+
}, null, 2);
|
|
3403
|
+
}
|
|
3404
|
+
try {
|
|
3405
|
+
const dryRun = args.dry_run === true || args.execute !== true;
|
|
3406
|
+
const result = await commandForNode(ctx, node, "fast_forward_mesh_node", {
|
|
3407
|
+
meshId: ctx.mesh.id,
|
|
3408
|
+
nodeId: node.id,
|
|
3409
|
+
workspace: node.workspace,
|
|
3410
|
+
branch: typeof args.branch === "string" ? args.branch : void 0,
|
|
3411
|
+
execute: args.execute === true && args.dry_run !== true,
|
|
3412
|
+
dryRun,
|
|
3413
|
+
updateSubmodules: args.update_submodules === true,
|
|
3414
|
+
submoduleIgnorePaths: submoduleIgnorePaths.length > 0 ? submoduleIgnorePaths : void 0
|
|
3415
|
+
});
|
|
3416
|
+
return JSON.stringify(unwrapCommandPayload(result), null, 2);
|
|
3417
|
+
} catch (e) {
|
|
3418
|
+
const failure = buildCoordinatorP2pRelayFailure(e, {
|
|
3419
|
+
command: "fast_forward_mesh_node",
|
|
3420
|
+
targetDaemonId: node.daemonId,
|
|
3421
|
+
nodeId: args.node_id
|
|
3422
|
+
});
|
|
3423
|
+
return JSON.stringify({
|
|
3424
|
+
...failure,
|
|
3425
|
+
workspace: node.workspace,
|
|
3426
|
+
allowed: false,
|
|
3427
|
+
willRun: false,
|
|
3428
|
+
executed: false,
|
|
3429
|
+
blockingReasons: [failure.code || "mesh_fast_forward_unavailable"]
|
|
3430
|
+
}, null, 2);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
2072
3433
|
async function meshCheckpoint(ctx, args) {
|
|
2073
3434
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2074
3435
|
if (node.policy?.readOnly) {
|
|
@@ -2084,7 +3445,13 @@ async function meshCheckpoint(ctx, args) {
|
|
|
2084
3445
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
2085
3446
|
kind: "checkpoint_created",
|
|
2086
3447
|
nodeId: args.node_id,
|
|
2087
|
-
payload: {
|
|
3448
|
+
payload: {
|
|
3449
|
+
message: args.message,
|
|
3450
|
+
commit: result?.checkpoint?.commit,
|
|
3451
|
+
outcome: result?.checkpoint?.status || (result?.checkpoint?.noop ? "skipped" : void 0),
|
|
3452
|
+
noop: result?.checkpoint?.noop === true,
|
|
3453
|
+
reason: result?.checkpoint?.reason
|
|
3454
|
+
}
|
|
2088
3455
|
});
|
|
2089
3456
|
} catch {
|
|
2090
3457
|
}
|
|
@@ -2100,7 +3467,13 @@ async function meshCheckpoint(ctx, args) {
|
|
|
2100
3467
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
2101
3468
|
kind: "checkpoint_created",
|
|
2102
3469
|
nodeId: args.node_id,
|
|
2103
|
-
payload: {
|
|
3470
|
+
payload: {
|
|
3471
|
+
message: args.message,
|
|
3472
|
+
commit: res?.checkpoint?.commit,
|
|
3473
|
+
outcome: res?.checkpoint?.status || (res?.checkpoint?.noop ? "skipped" : void 0),
|
|
3474
|
+
noop: res?.checkpoint?.noop === true,
|
|
3475
|
+
reason: res?.checkpoint?.reason
|
|
3476
|
+
}
|
|
2104
3477
|
});
|
|
2105
3478
|
} catch {
|
|
2106
3479
|
}
|
|
@@ -2115,7 +3488,7 @@ async function meshCheckpoint(ctx, args) {
|
|
|
2115
3488
|
async function meshApprove(ctx, args) {
|
|
2116
3489
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2117
3490
|
if (isLocalTransport(ctx.transport)) {
|
|
2118
|
-
const cached =
|
|
3491
|
+
const cached = getSessionMetadata(meshSessionCacheKey(args.node_id, args.session_id));
|
|
2119
3492
|
const providerSessionId = cached?.providerSessionId;
|
|
2120
3493
|
const result = await commandForNode(ctx, node, "resolve_action", {
|
|
2121
3494
|
sessionId: args.session_id,
|
|
@@ -2154,6 +3527,7 @@ async function meshCloneNode(ctx, args) {
|
|
|
2154
3527
|
if (existingIndex >= 0) ctx.mesh.nodes[existingIndex] = clonePayload.node;
|
|
2155
3528
|
else ctx.mesh.nodes.push(clonePayload.node);
|
|
2156
3529
|
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3530
|
+
await syncCoordinatorDaemonMeshCache(ctx);
|
|
2157
3531
|
}
|
|
2158
3532
|
return JSON.stringify(result, null, 2);
|
|
2159
3533
|
} else if (!isLocalTransport(ctx.transport) && sourceNode.daemonId) {
|
|
@@ -2171,6 +3545,7 @@ async function meshCloneNode(ctx, args) {
|
|
|
2171
3545
|
if (existingIndex >= 0) ctx.mesh.nodes[existingIndex] = clonePayload.node;
|
|
2172
3546
|
else ctx.mesh.nodes.push(clonePayload.node);
|
|
2173
3547
|
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3548
|
+
await syncCoordinatorDaemonMeshCache(ctx);
|
|
2174
3549
|
}
|
|
2175
3550
|
return JSON.stringify(res, null, 2);
|
|
2176
3551
|
} catch (e) {
|
|
@@ -2266,6 +3641,43 @@ async function meshRemoveNode(ctx, args) {
|
|
|
2266
3641
|
return JSON.stringify({ error: "Cloud mesh remove_node requires node daemonId" });
|
|
2267
3642
|
}
|
|
2268
3643
|
}
|
|
3644
|
+
function resolveRefineConfigNode(ctx, nodeId) {
|
|
3645
|
+
if (nodeId) return findNode(ctx.mesh, nodeId);
|
|
3646
|
+
const node = ctx.mesh.nodes.find((entry) => !!entry.workspace);
|
|
3647
|
+
if (!node) throw new Error("No mesh node with a workspace is available");
|
|
3648
|
+
return node;
|
|
3649
|
+
}
|
|
3650
|
+
async function meshRefineConfigSchema(ctx) {
|
|
3651
|
+
const node = resolveRefineConfigNode(ctx);
|
|
3652
|
+
const result = await commandForNode(ctx, node, "get_mesh_refine_config_schema", {});
|
|
3653
|
+
return JSON.stringify(result, null, 2);
|
|
3654
|
+
}
|
|
3655
|
+
async function meshValidateRefineConfig(ctx, args) {
|
|
3656
|
+
const node = resolveRefineConfigNode(ctx, args.node_id);
|
|
3657
|
+
const result = await commandForNode(ctx, node, "validate_mesh_refine_config", {
|
|
3658
|
+
workspace: node.workspace,
|
|
3659
|
+
inlineMesh: ctx.mesh,
|
|
3660
|
+
...args.config ? { config: args.config } : {}
|
|
3661
|
+
});
|
|
3662
|
+
return JSON.stringify(result, null, 2);
|
|
3663
|
+
}
|
|
3664
|
+
async function meshSuggestRefineConfig(ctx, args) {
|
|
3665
|
+
const node = resolveRefineConfigNode(ctx, args.node_id);
|
|
3666
|
+
const result = await commandForNode(ctx, node, "suggest_mesh_refine_config", {
|
|
3667
|
+
workspace: node.workspace,
|
|
3668
|
+
inlineMesh: ctx.mesh
|
|
3669
|
+
});
|
|
3670
|
+
return JSON.stringify(result, null, 2);
|
|
3671
|
+
}
|
|
3672
|
+
async function meshRefinePlan(ctx, args) {
|
|
3673
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3674
|
+
const result = await commandForNode(ctx, node, "plan_mesh_refine_node", {
|
|
3675
|
+
meshId: ctx.mesh.id,
|
|
3676
|
+
nodeId: args.node_id,
|
|
3677
|
+
inlineMesh: ctx.mesh
|
|
3678
|
+
});
|
|
3679
|
+
return JSON.stringify(result, null, 2);
|
|
3680
|
+
}
|
|
2269
3681
|
async function meshRefineNode(ctx, args) {
|
|
2270
3682
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2271
3683
|
if (isLocalTransport(ctx.transport)) {
|
|
@@ -2274,7 +3686,7 @@ async function meshRefineNode(ctx, args) {
|
|
|
2274
3686
|
nodeId: args.node_id,
|
|
2275
3687
|
inlineMesh: ctx.mesh
|
|
2276
3688
|
});
|
|
2277
|
-
if (result?.success && result.removeResult?.removed !== false) {
|
|
3689
|
+
if (result?.success && result.async !== true && result.removeResult?.removed !== false) {
|
|
2278
3690
|
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
2279
3691
|
if (idx >= 0) {
|
|
2280
3692
|
ctx.mesh.nodes.splice(idx, 1);
|
|
@@ -2289,7 +3701,7 @@ async function meshRefineNode(ctx, args) {
|
|
|
2289
3701
|
nodeId: args.node_id,
|
|
2290
3702
|
inlineMesh: ctx.mesh
|
|
2291
3703
|
});
|
|
2292
|
-
if (res?.success && res.removeResult?.removed !== false) {
|
|
3704
|
+
if (res?.success && res.async !== true && res.removeResult?.removed !== false) {
|
|
2293
3705
|
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
2294
3706
|
if (idx >= 0) {
|
|
2295
3707
|
ctx.mesh.nodes.splice(idx, 1);
|
|
@@ -2326,13 +3738,13 @@ var STANDARD_TOOLS = [
|
|
|
2326
3738
|
function buildMcpHelpText() {
|
|
2327
3739
|
const meshTools = ALL_MESH_TOOLS.map((tool) => tool.name);
|
|
2328
3740
|
return `
|
|
2329
|
-
|
|
3741
|
+
ADHDev MCP Server
|
|
2330
3742
|
|
|
2331
3743
|
Usage:
|
|
2332
|
-
adhdev
|
|
2333
|
-
adhdev
|
|
2334
|
-
adhdev
|
|
2335
|
-
adhdev-mcp --
|
|
3744
|
+
adhdev mcp Local mode (requires standalone daemon)
|
|
3745
|
+
adhdev mcp --api-key <key> Cloud mode (ADHDev cloud API)
|
|
3746
|
+
adhdev mcp --mode ipc --repo-mesh <mesh_id> Cloud daemon IPC mesh mode
|
|
3747
|
+
adhdev-mcp --help Compatibility bin (same server, legacy package entrypoint)
|
|
2336
3748
|
|
|
2337
3749
|
Options:
|
|
2338
3750
|
--mode <mode> Transport: local, cloud, or ipc
|
|
@@ -2357,6 +3769,7 @@ Mesh tools: ${meshTools.join(", ")}
|
|
|
2357
3769
|
// src/server.ts
|
|
2358
3770
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
2359
3771
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3772
|
+
var import_node_os = __toESM(require("os"));
|
|
2360
3773
|
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
2361
3774
|
|
|
2362
3775
|
// src/transports/local.ts
|
|
@@ -2511,8 +3924,8 @@ var CloudTransport = class {
|
|
|
2511
3924
|
if (!res.ok) throw new Error(`Approve failed: ${res.status}`);
|
|
2512
3925
|
return res.json();
|
|
2513
3926
|
}
|
|
2514
|
-
async gitStatus(daemonId, workspace, includeDiff = true) {
|
|
2515
|
-
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff) });
|
|
3927
|
+
async gitStatus(daemonId, workspace, includeDiff = true, refreshUpstream = false) {
|
|
3928
|
+
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff), refreshUpstream: String(refreshUpstream) });
|
|
2516
3929
|
const res = await fetch(
|
|
2517
3930
|
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-status?${params}`,
|
|
2518
3931
|
{ headers: this.headers() }
|
|
@@ -2907,6 +4320,28 @@ function formatChatResult(result, sessionId, format, limit = 50, compact = false
|
|
|
2907
4320
|
}))
|
|
2908
4321
|
}, null, 2);
|
|
2909
4322
|
}
|
|
4323
|
+
if ((format === "text" || format === void 0) && compact && compactPayload) {
|
|
4324
|
+
const summaryText = typeof compactPayload.summary === "string" ? compactPayload.summary.trim() : "";
|
|
4325
|
+
const tail = outputMessages.slice(-limit);
|
|
4326
|
+
const lastIndex = tail.length - 1;
|
|
4327
|
+
const lines2 = tail.flatMap((m, idx) => {
|
|
4328
|
+
const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
|
|
4329
|
+
const content = messageContent(m);
|
|
4330
|
+
if (idx === lastIndex && (role === "Agent" || m.role === "agent") && summaryText && content.trim() === summaryText) {
|
|
4331
|
+
return [];
|
|
4332
|
+
}
|
|
4333
|
+
const truncated = content.length > 500 ? `${content.slice(0, 500)}\u2026` : content;
|
|
4334
|
+
return [`[${role}] ${truncated}`];
|
|
4335
|
+
});
|
|
4336
|
+
if (compactPayload.summary) {
|
|
4337
|
+
const truncatedSummary = compactPayload.summary.length > 500 ? `${compactPayload.summary.slice(0, 500)}\u2026` : compactPayload.summary;
|
|
4338
|
+
lines2.push(`[Summary] ${truncatedSummary}`);
|
|
4339
|
+
}
|
|
4340
|
+
if (result?.pollingAdvisory) {
|
|
4341
|
+
lines2.push(`Advisory: ${result.pollingAdvisory.message}`);
|
|
4342
|
+
}
|
|
4343
|
+
return lines2.length > 0 ? lines2.join("\n\n") : "No messages in chat.";
|
|
4344
|
+
}
|
|
2910
4345
|
if (outputMessages.length === 0) {
|
|
2911
4346
|
return result?.pollingAdvisory ? `No messages in chat.
|
|
2912
4347
|
|
|
@@ -3011,6 +4446,90 @@ function formatChatDebugResult(result, options) {
|
|
|
3011
4446
|
return JSON.stringify(result, null, 2);
|
|
3012
4447
|
}
|
|
3013
4448
|
|
|
4449
|
+
// src/tools/spec-debug.ts
|
|
4450
|
+
var SPEC_DEBUG_TOOL = {
|
|
4451
|
+
name: "spec_debug",
|
|
4452
|
+
description: "Get current spec state, sections, and state transition history for a spec-driven CLI session (claude-cli, antigravity-cli, etc.). Use to diagnose idle/busy detection issues, inspect section parsing, or verify idle_hold and busy_hold behavior.",
|
|
4453
|
+
inputSchema: {
|
|
4454
|
+
type: "object",
|
|
4455
|
+
properties: {
|
|
4456
|
+
session_id: {
|
|
4457
|
+
type: "string",
|
|
4458
|
+
description: "Target session ID (from list_sessions)."
|
|
4459
|
+
},
|
|
4460
|
+
daemon_id: {
|
|
4461
|
+
type: "string",
|
|
4462
|
+
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
4463
|
+
},
|
|
4464
|
+
...FORMAT_PROP
|
|
4465
|
+
},
|
|
4466
|
+
required: ["session_id"]
|
|
4467
|
+
}
|
|
4468
|
+
};
|
|
4469
|
+
async function specDebug(transport, args) {
|
|
4470
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id.trim() : "";
|
|
4471
|
+
if (!sessionId) throw new Error("session_id is required");
|
|
4472
|
+
let result;
|
|
4473
|
+
if (isLocalTransport(transport)) {
|
|
4474
|
+
result = await transport.command("get_spec_debug", { targetSessionId: sessionId });
|
|
4475
|
+
} else {
|
|
4476
|
+
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
4477
|
+
const targetId = `${args.daemon_id}:session:${sessionId}`;
|
|
4478
|
+
result = await transport.sendCommand(targetId, "get_spec_debug", { targetSessionId: sessionId });
|
|
4479
|
+
}
|
|
4480
|
+
return formatSpecDebugResult(result, { sessionId, format: args.format });
|
|
4481
|
+
}
|
|
4482
|
+
function formatSpecDebugResult(result, options) {
|
|
4483
|
+
if (!result?.success) {
|
|
4484
|
+
const err = result?.error || "Unknown error";
|
|
4485
|
+
if (options.format === "json") return JSON.stringify({ success: false, error: err }, null, 2);
|
|
4486
|
+
return `Error: ${err}`;
|
|
4487
|
+
}
|
|
4488
|
+
if (options.format === "json") return JSON.stringify(result, null, 2);
|
|
4489
|
+
const snap = result.snapshot;
|
|
4490
|
+
if (!snap) {
|
|
4491
|
+
return [
|
|
4492
|
+
`session_id: ${options.sessionId}`,
|
|
4493
|
+
`provider_type: ${String(result.providerType || "")}`,
|
|
4494
|
+
"is_spec_provider: false",
|
|
4495
|
+
"No spec debug data available (not a spec-driven provider)."
|
|
4496
|
+
].join("\n");
|
|
4497
|
+
}
|
|
4498
|
+
const lines = [];
|
|
4499
|
+
lines.push(`session_id: ${options.sessionId}`);
|
|
4500
|
+
lines.push(`provider_type: ${String(result.providerType || snap.cliType || "")}`);
|
|
4501
|
+
lines.push(`spec_id: ${String(snap.spec_id || "")}`);
|
|
4502
|
+
lines.push(`spec_path: ${String(snap.specPath || "")}`);
|
|
4503
|
+
lines.push(`current_state: ${snap.current_state ? `${snap.current_state.id} (${snap.current_state.label})` : "none"}`);
|
|
4504
|
+
lines.push(`idle_hold_pending: ${String(snap.idleHoldPending ?? false)}`);
|
|
4505
|
+
lines.push(`last_busy_at: ${snap.lastBusyAt ? new Date(snap.lastBusyAt).toISOString() : "never"}`);
|
|
4506
|
+
lines.push(`exited: ${String(snap.exited ?? false)}`);
|
|
4507
|
+
if (snap.current_modal) {
|
|
4508
|
+
lines.push(`current_modal: ${JSON.stringify(snap.current_modal)}`);
|
|
4509
|
+
}
|
|
4510
|
+
if (snap.sections && typeof snap.sections === "object") {
|
|
4511
|
+
lines.push("");
|
|
4512
|
+
lines.push("\u2500\u2500 sections \u2500\u2500");
|
|
4513
|
+
for (const [id, text] of Object.entries(snap.sections)) {
|
|
4514
|
+
const preview = String(text || "").replace(/\n/g, "\u21B5").slice(0, 120);
|
|
4515
|
+
lines.push(` ${id}: ${preview}`);
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
const history = Array.isArray(snap.stateHistory) ? snap.stateHistory : [];
|
|
4519
|
+
if (history.length > 0) {
|
|
4520
|
+
lines.push("");
|
|
4521
|
+
lines.push("\u2500\u2500 state history (newest first) \u2500\u2500");
|
|
4522
|
+
const now = Date.now();
|
|
4523
|
+
for (const entry of [...history].reverse().slice(0, 20)) {
|
|
4524
|
+
const agoMs = now - entry.at;
|
|
4525
|
+
const ago = agoMs < 2e3 ? `${agoMs}ms ago` : `${(agoMs / 1e3).toFixed(1)}s ago`;
|
|
4526
|
+
const dur = entry.durationMs > 0 ? ` held ${entry.durationMs}ms` : "";
|
|
4527
|
+
lines.push(` ${String(entry.stateId).padEnd(18)} ${ago}${dur}`);
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
return lines.join("\n");
|
|
4531
|
+
}
|
|
4532
|
+
|
|
3014
4533
|
// src/tools/send-chat.ts
|
|
3015
4534
|
var SEND_CHAT_TOOL = {
|
|
3016
4535
|
name: "send_chat",
|
|
@@ -3874,6 +5393,7 @@ async function startMcpServer(opts) {
|
|
|
3874
5393
|
requirePreTaskCheckpoint: false,
|
|
3875
5394
|
requirePostTaskCheckpoint: true,
|
|
3876
5395
|
requireApprovalForPush: true,
|
|
5396
|
+
allowAutoPublishSubmoduleMainCommits: false,
|
|
3877
5397
|
requireApprovalForDestructiveGit: true,
|
|
3878
5398
|
dirtyWorkspaceBehavior: "warn",
|
|
3879
5399
|
maxParallelTasks: 2,
|
|
@@ -3930,11 +5450,13 @@ async function startMcpServer(opts) {
|
|
|
3930
5450
|
}
|
|
3931
5451
|
let localDaemonId;
|
|
3932
5452
|
let localMachineId;
|
|
5453
|
+
let coordinatorHostname = import_node_os.default.hostname();
|
|
3933
5454
|
if (transport instanceof LocalTransport || transport instanceof IpcTransport) {
|
|
3934
5455
|
try {
|
|
3935
5456
|
const { loadConfig } = await import("@adhdev/daemon-core");
|
|
3936
5457
|
const cfg = loadConfig();
|
|
3937
|
-
if (cfg.
|
|
5458
|
+
if (cfg.machineId) localMachineId = cfg.machineId;
|
|
5459
|
+
else if (cfg.registeredMachineId) localMachineId = cfg.registeredMachineId;
|
|
3938
5460
|
} catch {
|
|
3939
5461
|
}
|
|
3940
5462
|
}
|
|
@@ -3942,14 +5464,16 @@ async function startMcpServer(opts) {
|
|
|
3942
5464
|
try {
|
|
3943
5465
|
const statusResult = await transport.getStatus();
|
|
3944
5466
|
const instanceId = typeof statusResult?.status?.instanceId === "string" ? statusResult.status.instanceId.trim() : "";
|
|
5467
|
+
const hostname = typeof statusResult?.status?.hostname === "string" ? statusResult.status.hostname.trim() : typeof statusResult?.status?.machine?.hostname === "string" ? statusResult.status.machine.hostname.trim() : "";
|
|
3945
5468
|
if (instanceId) localDaemonId = instanceId;
|
|
5469
|
+
if (hostname) coordinatorHostname = hostname;
|
|
3946
5470
|
} catch {
|
|
3947
5471
|
}
|
|
3948
5472
|
}
|
|
3949
|
-
const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {}, ...localMachineId ? { localMachineId } : {} };
|
|
5473
|
+
const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {}, ...localMachineId ? { localMachineId } : {}, ...coordinatorHostname ? { coordinatorHostname } : {} };
|
|
3950
5474
|
const coordinatorPrompt = await buildMeshModeCoordinatorPrompt(mesh);
|
|
3951
5475
|
const server2 = new import_server.Server(
|
|
3952
|
-
{ name: "adhdev-mcp-server", version: "0.9.
|
|
5476
|
+
{ name: "adhdev-mcp-server", version: "0.9.82" },
|
|
3953
5477
|
{ capabilities: { tools: {}, resources: {} } }
|
|
3954
5478
|
);
|
|
3955
5479
|
const { ListResourcesRequestSchema, ReadResourceRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
@@ -3975,7 +5499,7 @@ async function startMcpServer(opts) {
|
|
|
3975
5499
|
let text;
|
|
3976
5500
|
switch (name) {
|
|
3977
5501
|
case "mesh_status":
|
|
3978
|
-
text = await meshStatus(meshCtx);
|
|
5502
|
+
text = await meshStatus(meshCtx, a);
|
|
3979
5503
|
break;
|
|
3980
5504
|
case "mesh_list_nodes":
|
|
3981
5505
|
text = await meshListNodes(meshCtx);
|
|
@@ -4007,6 +5531,9 @@ async function startMcpServer(opts) {
|
|
|
4007
5531
|
case "mesh_git_status":
|
|
4008
5532
|
text = await meshGitStatus(meshCtx, a);
|
|
4009
5533
|
break;
|
|
5534
|
+
case "mesh_fast_forward_node":
|
|
5535
|
+
text = await meshFastForwardNode(meshCtx, a);
|
|
5536
|
+
break;
|
|
4010
5537
|
case "mesh_checkpoint":
|
|
4011
5538
|
text = await meshCheckpoint(meshCtx, a);
|
|
4012
5539
|
break;
|
|
@@ -4022,6 +5549,18 @@ async function startMcpServer(opts) {
|
|
|
4022
5549
|
case "mesh_refine_node":
|
|
4023
5550
|
text = await meshRefineNode(meshCtx, a);
|
|
4024
5551
|
break;
|
|
5552
|
+
case "mesh_refine_config_schema":
|
|
5553
|
+
text = await meshRefineConfigSchema(meshCtx);
|
|
5554
|
+
break;
|
|
5555
|
+
case "mesh_validate_refine_config":
|
|
5556
|
+
text = await meshValidateRefineConfig(meshCtx, a);
|
|
5557
|
+
break;
|
|
5558
|
+
case "mesh_suggest_refine_config":
|
|
5559
|
+
text = await meshSuggestRefineConfig(meshCtx, a);
|
|
5560
|
+
break;
|
|
5561
|
+
case "mesh_refine_plan":
|
|
5562
|
+
text = await meshRefinePlan(meshCtx, a);
|
|
5563
|
+
break;
|
|
4025
5564
|
case "mesh_cleanup_sessions":
|
|
4026
5565
|
text = await meshCleanupSessions(meshCtx, a);
|
|
4027
5566
|
break;
|
|
@@ -4053,6 +5592,7 @@ async function startMcpServer(opts) {
|
|
|
4053
5592
|
CHECK_PENDING_TOOL,
|
|
4054
5593
|
READ_CHAT_TOOL,
|
|
4055
5594
|
READ_CHAT_DEBUG_TOOL,
|
|
5595
|
+
SPEC_DEBUG_TOOL,
|
|
4056
5596
|
SEND_CHAT_TOOL,
|
|
4057
5597
|
APPROVE_TOOL,
|
|
4058
5598
|
GIT_STATUS_TOOL,
|
|
@@ -4088,6 +5628,10 @@ async function startMcpServer(opts) {
|
|
|
4088
5628
|
const text = await readChatDebug(transport, a);
|
|
4089
5629
|
return { content: [{ type: "text", text }] };
|
|
4090
5630
|
}
|
|
5631
|
+
case "spec_debug": {
|
|
5632
|
+
const text = await specDebug(transport, a);
|
|
5633
|
+
return { content: [{ type: "text", text }] };
|
|
5634
|
+
}
|
|
4091
5635
|
case "send_chat": {
|
|
4092
5636
|
const text = await sendChat(transport, { message: a.message, session_id: a.session_id, daemon_id: a.daemon_id });
|
|
4093
5637
|
return { content: [{ type: "text", text }] };
|