@adhdev/daemon-standalone 0.9.82-rc.25 → 0.9.82-rc.250
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 +61330 -34379
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/public/assets/index-BBMuaB00.js +113 -0
- package/public/assets/index-DW-XJSIP.css +1 -0
- package/public/assets/{terminal-CfAvHQvJ.js → terminal-DsCRNxW_.js} +4 -4
- package/public/assets/vendor-BHUMCOj6.js +2768 -0
- package/public/index.html +3 -3
- package/public/snake.html +229 -0
- package/vendor/mcp-server/index.js +1876 -1376
- 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-CiB8XPwO.css +0 -1
- package/public/assets/index-Dt2Xvr87.js +0 -98
- package/public/assets/vendor-CgiI0UIA.js +0 -2745
|
@@ -35,9 +35,128 @@ __export(index_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
|
+
// src/tools/mesh-tools.ts
|
|
39
|
+
var import_node_crypto = require("crypto");
|
|
40
|
+
|
|
38
41
|
// src/transports/ipc.ts
|
|
39
42
|
var DEFAULT_IPC_PORT = 19222;
|
|
40
43
|
var DEFAULT_IPC_PATH = "/ipc";
|
|
44
|
+
var DEFAULT_IPC_COMMAND_TIMEOUT_MS = 15e3;
|
|
45
|
+
var IPC_COMMAND_TIMEOUTS_MS = {
|
|
46
|
+
mesh_relay_command: 12e4,
|
|
47
|
+
agent_command: 3e4,
|
|
48
|
+
git_status: 45e3,
|
|
49
|
+
git_diff_summary: 45e3,
|
|
50
|
+
fast_forward_mesh_node: 12e4,
|
|
51
|
+
mesh_status: 12e4
|
|
52
|
+
};
|
|
53
|
+
var WS_CONNECTING = 0;
|
|
54
|
+
var WS_OPEN = 1;
|
|
55
|
+
var POOL_IDLE_EVICT_MS = 5 * 6e4;
|
|
56
|
+
var POOL_MAX_AGE_MS = 10 * 6e4;
|
|
57
|
+
var connectionPool = /* @__PURE__ */ new Map();
|
|
58
|
+
function buildRequestId() {
|
|
59
|
+
return `mcp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
60
|
+
}
|
|
61
|
+
function getTimeoutMs(type, nestedCommand) {
|
|
62
|
+
return Math.max(
|
|
63
|
+
IPC_COMMAND_TIMEOUTS_MS[type] ?? DEFAULT_IPC_COMMAND_TIMEOUT_MS,
|
|
64
|
+
IPC_COMMAND_TIMEOUTS_MS[nestedCommand] ?? DEFAULT_IPC_COMMAND_TIMEOUT_MS
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function getOrCreateConnection(WebSocketCtor, url) {
|
|
68
|
+
const existing = connectionPool.get(url);
|
|
69
|
+
if (existing) {
|
|
70
|
+
const { readyState } = existing.ws;
|
|
71
|
+
const now2 = Date.now();
|
|
72
|
+
const isAlive = readyState === WS_CONNECTING || readyState === WS_OPEN;
|
|
73
|
+
const isIdle = now2 - existing.lastUsedAt > POOL_IDLE_EVICT_MS && existing.pending.size === 0;
|
|
74
|
+
const isTooOld = now2 - existing.createdAt > POOL_MAX_AGE_MS && existing.pending.size === 0;
|
|
75
|
+
if (isAlive && !isIdle && !isTooOld) {
|
|
76
|
+
return existing;
|
|
77
|
+
}
|
|
78
|
+
if (isAlive && (isIdle || isTooOld)) {
|
|
79
|
+
try {
|
|
80
|
+
existing.ws.close();
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
connectionPool.delete(url);
|
|
84
|
+
}
|
|
85
|
+
connectionPool.delete(url);
|
|
86
|
+
}
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const conn = {
|
|
89
|
+
ws: new WebSocketCtor(url),
|
|
90
|
+
ready: false,
|
|
91
|
+
commandQueue: [],
|
|
92
|
+
pending: /* @__PURE__ */ new Map(),
|
|
93
|
+
lastUsedAt: now,
|
|
94
|
+
createdAt: now
|
|
95
|
+
};
|
|
96
|
+
connectionPool.set(url, conn);
|
|
97
|
+
const drainQueue = () => {
|
|
98
|
+
conn.ready = true;
|
|
99
|
+
for (const { type, args, requestId } of conn.commandQueue) {
|
|
100
|
+
conn.ws.send(JSON.stringify({ type: "ext:command", payload: { command: type, args, requestId } }));
|
|
101
|
+
}
|
|
102
|
+
conn.commandQueue = [];
|
|
103
|
+
};
|
|
104
|
+
let tornDown = false;
|
|
105
|
+
const teardown = (error) => {
|
|
106
|
+
if (tornDown) return;
|
|
107
|
+
tornDown = true;
|
|
108
|
+
connectionPool.delete(url);
|
|
109
|
+
conn.ready = false;
|
|
110
|
+
for (const [, req] of conn.pending) {
|
|
111
|
+
clearTimeout(req.timer);
|
|
112
|
+
req.reject(error);
|
|
113
|
+
}
|
|
114
|
+
conn.pending.clear();
|
|
115
|
+
conn.commandQueue = [];
|
|
116
|
+
};
|
|
117
|
+
conn.ws.addEventListener("open", () => {
|
|
118
|
+
conn.ws.send(JSON.stringify({
|
|
119
|
+
type: "ext:register",
|
|
120
|
+
payload: {
|
|
121
|
+
ideType: "mcp-server",
|
|
122
|
+
ideVersion: "1.0.0",
|
|
123
|
+
extensionVersion: "1.0.0",
|
|
124
|
+
instanceId: `mcp-server-${process.pid}`,
|
|
125
|
+
machineId: "mcp-server",
|
|
126
|
+
workspaceFolders: []
|
|
127
|
+
}
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
130
|
+
conn.ws.addEventListener("message", (event) => {
|
|
131
|
+
try {
|
|
132
|
+
const raw = typeof event.data === "string" ? event.data : String(event.data);
|
|
133
|
+
const msg = JSON.parse(raw);
|
|
134
|
+
if (msg?.type === "daemon:welcome") {
|
|
135
|
+
drainQueue();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (msg?.type !== "ext:command_result") return;
|
|
139
|
+
const req = conn.pending.get(msg?.payload?.requestId);
|
|
140
|
+
if (!req) return;
|
|
141
|
+
conn.pending.delete(msg.payload.requestId);
|
|
142
|
+
clearTimeout(req.timer);
|
|
143
|
+
const payload = msg.payload;
|
|
144
|
+
if (payload?.success === false) {
|
|
145
|
+
req.reject(new Error(payload.error || "Daemon IPC command failed"));
|
|
146
|
+
} else {
|
|
147
|
+
req.resolve(payload?.result ?? payload);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
conn.ws.addEventListener("error", () => {
|
|
153
|
+
teardown(new Error(`Cannot connect to daemon IPC at ${url}`));
|
|
154
|
+
});
|
|
155
|
+
conn.ws.addEventListener("close", () => {
|
|
156
|
+
teardown(new Error(`Daemon IPC connection closed: ${url}`));
|
|
157
|
+
});
|
|
158
|
+
return conn;
|
|
159
|
+
}
|
|
41
160
|
var IpcTransport = class {
|
|
42
161
|
port;
|
|
43
162
|
path;
|
|
@@ -66,87 +185,46 @@ 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
|
};
|
|
139
226
|
|
|
140
|
-
// src/transports/mode.ts
|
|
141
|
-
function isLocalTransport(transport) {
|
|
142
|
-
return typeof transport.command === "function";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
227
|
// src/tools/chat-compact.ts
|
|
146
|
-
function isAssistantLike(message) {
|
|
147
|
-
const role = String(message?.role ?? "").toLowerCase();
|
|
148
|
-
return role === "assistant" || role === "agent";
|
|
149
|
-
}
|
|
150
228
|
function messageContent(message) {
|
|
151
229
|
const content = message?.content;
|
|
152
230
|
if (typeof content === "string") return content;
|
|
@@ -165,11 +243,36 @@ function isCoordinatorVisibleMessage(message) {
|
|
|
165
243
|
if (meta?.internal === true || meta?.debug === true || meta?.control === true || meta?.userVisible === false || meta?.user_visible === false) return false;
|
|
166
244
|
return role === "user" || role === "assistant" || role === "agent";
|
|
167
245
|
}
|
|
246
|
+
function summarizeToolMessage(message) {
|
|
247
|
+
if (!message || typeof message !== "object") return null;
|
|
248
|
+
const kind = String(message.kind ?? message.type ?? message.messageKind ?? "").toLowerCase();
|
|
249
|
+
const role = String(message.role ?? "").toLowerCase();
|
|
250
|
+
if (kind === "terminal" || kind === "bash") {
|
|
251
|
+
const cmd = message.command ?? message.cmd ?? message.input ?? messageContent(message);
|
|
252
|
+
const exit = message.exitCode ?? message.exit_code ?? message.code;
|
|
253
|
+
const cmdShort = typeof cmd === "string" ? cmd.split("\n")[0].slice(0, 120) : null;
|
|
254
|
+
if (!cmdShort) return null;
|
|
255
|
+
return exit !== void 0 && exit !== null ? `[Bash] ${cmdShort} \u2192 exit ${exit}` : `[Bash] ${cmdShort}`;
|
|
256
|
+
}
|
|
257
|
+
if (kind === "tool_call" || kind === "tool" || role === "tool") {
|
|
258
|
+
const name = message.name ?? message.toolName ?? message.tool_name ?? message.function?.name;
|
|
259
|
+
if (typeof name === "string" && name.trim()) return `[Tool] ${name.trim()}`;
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
if (kind === "tool_result") {
|
|
263
|
+
const exit = message.exitCode ?? message.exit_code ?? message.code;
|
|
264
|
+
const name = message.name ?? message.toolName ?? message.tool_name;
|
|
265
|
+
const label = typeof name === "string" && name.trim() ? name.trim() : "tool";
|
|
266
|
+
return exit !== void 0 && exit !== null ? `[Tool result: ${label}] exit ${exit}` : null;
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
168
270
|
function buildCompactMessageTail(visibleMessages, opts) {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
271
|
+
const tail = visibleMessages.slice(-opts.limit);
|
|
272
|
+
if (opts.finalAssistant && !tail.includes(opts.finalAssistant)) {
|
|
273
|
+
return [opts.finalAssistant, ...tail];
|
|
274
|
+
}
|
|
275
|
+
return tail;
|
|
173
276
|
}
|
|
174
277
|
function compactChatPayload(payload, opts = {}) {
|
|
175
278
|
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
@@ -181,6 +284,9 @@ function compactChatPayload(payload, opts = {}) {
|
|
|
181
284
|
});
|
|
182
285
|
const summary = typeof payload?.summary === "string" && payload.summary.trim() ? payload.summary.trim() : messageContent(finalAssistant).trim();
|
|
183
286
|
const messages = buildCompactMessageTail(visible, { summary, finalAssistant, limit });
|
|
287
|
+
const toolSummaries = rawMessages.filter((m) => !isCoordinatorVisibleMessage(m)).map(summarizeToolMessage).filter((s) => s !== null);
|
|
288
|
+
const omittedMessages = Math.max(0, rawMessages.length - messages.length);
|
|
289
|
+
const filteredMessages = Math.max(0, rawMessages.length - visible.length);
|
|
184
290
|
return {
|
|
185
291
|
success: payload?.success !== false,
|
|
186
292
|
compact: true,
|
|
@@ -190,8 +296,9 @@ function compactChatPayload(payload, opts = {}) {
|
|
|
190
296
|
providerSessionId: payload?.providerSessionId ?? null,
|
|
191
297
|
totalMessages: rawMessages.length,
|
|
192
298
|
visibleMessages: visible.length,
|
|
193
|
-
filteredMessages
|
|
194
|
-
omittedMessages
|
|
299
|
+
filteredMessages,
|
|
300
|
+
omittedMessages,
|
|
301
|
+
...toolSummaries.length > 0 ? { toolSummaries } : {},
|
|
195
302
|
summary,
|
|
196
303
|
...payload?.changedFiles !== void 0 ? { changedFiles: payload.changedFiles } : {},
|
|
197
304
|
...payload?.testsRun !== void 0 ? { testsRun: payload.testsRun } : {},
|
|
@@ -241,17 +348,63 @@ function annotateRapidReadChatAdvisory(payload, options) {
|
|
|
241
348
|
|
|
242
349
|
// src/tools/mesh-tools.ts
|
|
243
350
|
var import_daemon_core = require("@adhdev/daemon-core");
|
|
351
|
+
var SESSION_PROVIDER_METADATA_TTL_MS = 30 * 6e4;
|
|
244
352
|
var meshSessionProviderMetadata = /* @__PURE__ */ new Map();
|
|
353
|
+
function getSessionMetadata(key) {
|
|
354
|
+
const entry = meshSessionProviderMetadata.get(key);
|
|
355
|
+
if (!entry) return void 0;
|
|
356
|
+
if (entry.expiresAt <= Date.now()) {
|
|
357
|
+
meshSessionProviderMetadata.delete(key);
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
return entry;
|
|
361
|
+
}
|
|
362
|
+
var ACTIVE_WORK_POLLING_BACKOFF_MS = 6e4;
|
|
363
|
+
function buildActiveWorkPollingGuidance(summary, now = Date.now()) {
|
|
364
|
+
if (!summary || summary.generatingCount <= 0) return void 0;
|
|
365
|
+
return {
|
|
366
|
+
activeGeneratingWork: true,
|
|
367
|
+
generatingCount: summary.generatingCount,
|
|
368
|
+
doNotPollBefore: new Date(now + ACTIVE_WORK_POLLING_BACKOFF_MS).toISOString(),
|
|
369
|
+
eventSurface: "pendingCoordinatorEvents",
|
|
370
|
+
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.",
|
|
371
|
+
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."
|
|
372
|
+
};
|
|
373
|
+
}
|
|
245
374
|
function readString(value) {
|
|
246
375
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
247
376
|
}
|
|
377
|
+
function summarizeTaskMessage(message) {
|
|
378
|
+
const taskSummary = message.replace(/\s+/g, " ").trim();
|
|
379
|
+
const taskTitle = taskSummary.length > 96 ? `${taskSummary.slice(0, 93)}...` : taskSummary;
|
|
380
|
+
return { taskTitle: taskTitle || "(untitled task)", taskSummary };
|
|
381
|
+
}
|
|
382
|
+
function buildDirectTaskPayload(message, via, opts) {
|
|
383
|
+
const descriptor = summarizeTaskMessage(message);
|
|
384
|
+
return {
|
|
385
|
+
source: "direct",
|
|
386
|
+
via,
|
|
387
|
+
taskId: opts.taskId,
|
|
388
|
+
message,
|
|
389
|
+
taskTitle: descriptor.taskTitle,
|
|
390
|
+
taskSummary: descriptor.taskSummary,
|
|
391
|
+
...opts.taskMode ? { taskMode: opts.taskMode } : {},
|
|
392
|
+
...opts.providerType ? { providerType: opts.providerType } : {},
|
|
393
|
+
...opts.targetSessionId ? { targetSessionId: opts.targetSessionId } : {},
|
|
394
|
+
...opts.dispatchedToIdleSession !== void 0 ? { dispatchedToIdleSession: opts.dispatchedToIdleSession } : {}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function findNode(mesh, nodeId) {
|
|
398
|
+
const node = mesh.nodes.find((n) => n.id === nodeId);
|
|
399
|
+
if (!node) throw new Error(`Node '${nodeId}' is not a member of mesh '${mesh.name}'`);
|
|
400
|
+
return node;
|
|
401
|
+
}
|
|
248
402
|
var DUPLICATE_DISPATCH_WINDOW_MS = 6e4;
|
|
249
403
|
var STALE_ASSIGNED_QUEUE_MS = 30 * 6e4;
|
|
250
404
|
var OLD_HISTORICAL_QUEUE_RECORD_MS = 7 * 24 * 60 * 6e4;
|
|
251
405
|
var ACTIVE_QUEUE_STATUSES = /* @__PURE__ */ new Set(["pending", "assigned"]);
|
|
252
406
|
var HISTORICAL_QUEUE_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
253
407
|
async function refreshMeshFromDaemon(ctx) {
|
|
254
|
-
if (!(ctx.transport instanceof IpcTransport)) return;
|
|
255
408
|
try {
|
|
256
409
|
const result = await ctx.transport.command("get_mesh", { meshId: ctx.mesh.id });
|
|
257
410
|
if (!result?.success || !Array.isArray(result.mesh?.nodes)) return;
|
|
@@ -412,6 +565,25 @@ function extractStatusMetadataSessions(value) {
|
|
|
412
565
|
function resolveSessionProviderType(session) {
|
|
413
566
|
return readString(session?.providerType) || readString(session?.cliType) || readString(session?.agentType) || "";
|
|
414
567
|
}
|
|
568
|
+
function isMeshCoordinatorSessionRecord(session) {
|
|
569
|
+
return Boolean(
|
|
570
|
+
readString(session?.settings?.meshCoordinatorFor) || readString(session?.meta?.meshCoordinatorFor) || readString(session?.metadata?.meshCoordinatorFor) || readString(session?.meshCoordinatorFor)
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
function isUnmanagedSessionRecord(session) {
|
|
574
|
+
const hasMeshNodeFor = Boolean(
|
|
575
|
+
readString(session?.settings?.meshNodeFor) || readString(session?.meta?.meshNodeFor) || readString(session?.metadata?.meshNodeFor) || readString(session?.meshNodeFor)
|
|
576
|
+
);
|
|
577
|
+
if (hasMeshNodeFor) return false;
|
|
578
|
+
if (isMeshCoordinatorSessionRecord(session)) return false;
|
|
579
|
+
const launchedByCoordinator = Boolean(
|
|
580
|
+
session?.settings?.launchedByCoordinator === true || session?.meta?.launchedByCoordinator === true || session?.launchedByCoordinator === true
|
|
581
|
+
);
|
|
582
|
+
return !launchedByCoordinator;
|
|
583
|
+
}
|
|
584
|
+
function isWorkerTaskMode(taskMode) {
|
|
585
|
+
return taskMode !== "live_debug_readonly";
|
|
586
|
+
}
|
|
415
587
|
function addSessionRecord(target, session) {
|
|
416
588
|
if (!session || typeof session !== "object" || isTerminalSessionRecord(session)) return;
|
|
417
589
|
const sessionId = readSessionRecordId(session);
|
|
@@ -480,18 +652,26 @@ function queueAssignmentStaleReason(task, liveness) {
|
|
|
480
652
|
}
|
|
481
653
|
function buildQueueStatusSummary(queue) {
|
|
482
654
|
const counts = { pending: 0, assigned: 0, completed: 0, failed: 0, cancelled: 0 };
|
|
655
|
+
let staleAssigned = 0;
|
|
483
656
|
for (const task of queue) {
|
|
484
657
|
const status = typeof task?.status === "string" ? task.status : void 0;
|
|
485
658
|
if (status && Object.prototype.hasOwnProperty.call(counts, status)) {
|
|
486
659
|
counts[status] += 1;
|
|
487
660
|
}
|
|
661
|
+
if (status === "assigned" && task?.staleAssigned === true) staleAssigned += 1;
|
|
488
662
|
}
|
|
663
|
+
const liveAssigned = Math.max(0, counts.assigned - staleAssigned);
|
|
489
664
|
return {
|
|
490
665
|
totalCount: queue.length,
|
|
491
|
-
activeCount: counts.pending +
|
|
666
|
+
activeCount: counts.pending + liveAssigned,
|
|
492
667
|
historicalCount: counts.completed + counts.failed + counts.cancelled,
|
|
493
668
|
counts,
|
|
494
669
|
activeCounts: {
|
|
670
|
+
pending: counts.pending,
|
|
671
|
+
assigned: liveAssigned
|
|
672
|
+
},
|
|
673
|
+
staleAssignedCount: staleAssigned,
|
|
674
|
+
rawActiveCounts: {
|
|
495
675
|
pending: counts.pending,
|
|
496
676
|
assigned: counts.assigned
|
|
497
677
|
},
|
|
@@ -519,6 +699,18 @@ function filterQueueForView(queue, view, statuses) {
|
|
|
519
699
|
if (view === "historical") return queue.filter((task) => HISTORICAL_QUEUE_STATUSES.has(String(task?.status || "")));
|
|
520
700
|
return queue;
|
|
521
701
|
}
|
|
702
|
+
function prioritizeActiveQueueRows(queue) {
|
|
703
|
+
const active = [];
|
|
704
|
+
const historical = [];
|
|
705
|
+
const other = [];
|
|
706
|
+
for (const task of queue) {
|
|
707
|
+
const status = String(task?.status || "");
|
|
708
|
+
if (ACTIVE_QUEUE_STATUSES.has(status)) active.push(task);
|
|
709
|
+
else if (HISTORICAL_QUEUE_STATUSES.has(status)) historical.push(task);
|
|
710
|
+
else other.push(task);
|
|
711
|
+
}
|
|
712
|
+
return [...active, ...other, ...historical];
|
|
713
|
+
}
|
|
522
714
|
function slimQueueTask(task) {
|
|
523
715
|
return {
|
|
524
716
|
id: task?.id,
|
|
@@ -612,6 +804,169 @@ function unwrapCommandPayload(value) {
|
|
|
612
804
|
}
|
|
613
805
|
return current;
|
|
614
806
|
}
|
|
807
|
+
function isDirectDispatchLedgerEntry(entry) {
|
|
808
|
+
if (entry?.kind !== "task_dispatched") return false;
|
|
809
|
+
const payload = entry.payload || {};
|
|
810
|
+
const via = readString(payload.via);
|
|
811
|
+
return payload.source === "direct" || via === "p2p_direct" || via === "local_direct" || via === "mesh_send_task";
|
|
812
|
+
}
|
|
813
|
+
function readMessageTimestampIso(message) {
|
|
814
|
+
for (const value of [message?.timestamp, message?.createdAt, message?.created_at, message?.updatedAt, message?.time]) {
|
|
815
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
816
|
+
const ms = value > 1e10 ? value : value * 1e3;
|
|
817
|
+
return new Date(ms).toISOString();
|
|
818
|
+
}
|
|
819
|
+
if (typeof value === "string" && value.trim()) {
|
|
820
|
+
const ms = new Date(value.trim()).getTime();
|
|
821
|
+
if (Number.isFinite(ms)) return new Date(ms).toISOString();
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return void 0;
|
|
825
|
+
}
|
|
826
|
+
function readFinalAssistantTranscriptEvidence(payload) {
|
|
827
|
+
const rawMessages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
828
|
+
const finalAssistant = [...rawMessages].reverse().filter(isCoordinatorVisibleMessage).find((message) => {
|
|
829
|
+
const role = String(message?.role ?? "").toLowerCase();
|
|
830
|
+
return (role === "assistant" || role === "agent") && messageContent(message).trim();
|
|
831
|
+
});
|
|
832
|
+
const finalSummary = messageContent(finalAssistant).trim() || (typeof payload?.summary === "string" && payload.summary.trim() ? payload.summary.trim() : void 0);
|
|
833
|
+
return {
|
|
834
|
+
finalSummary,
|
|
835
|
+
transcriptMessageAt: finalAssistant ? readMessageTimestampIso(finalAssistant) : void 0
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function findNodeSession(nodes, nodeId, sessionId) {
|
|
839
|
+
if (!nodeId || !sessionId) return {};
|
|
840
|
+
const node = nodes.find((candidate) => readString(candidate?.id) === nodeId || readString(candidate?.nodeId) === nodeId);
|
|
841
|
+
if (!node) return {};
|
|
842
|
+
const sessions = Array.isArray(node.sessions) ? node.sessions : [];
|
|
843
|
+
const session = sessions.find((candidate) => readSessionRecordId(candidate) === sessionId);
|
|
844
|
+
return { node, session };
|
|
845
|
+
}
|
|
846
|
+
function buildDirectDispatchReconciliationCandidates(directDispatches, ledgerEntries) {
|
|
847
|
+
const candidates = [];
|
|
848
|
+
const seenTaskIds = /* @__PURE__ */ new Set();
|
|
849
|
+
for (const dispatch of directDispatches || []) {
|
|
850
|
+
const taskId = readString(dispatch?.taskId);
|
|
851
|
+
if (!taskId || seenTaskIds.has(taskId)) continue;
|
|
852
|
+
seenTaskIds.add(taskId);
|
|
853
|
+
candidates.push(dispatch);
|
|
854
|
+
}
|
|
855
|
+
for (const entry of ledgerEntries || []) {
|
|
856
|
+
if (!isDirectDispatchLedgerEntry(entry)) continue;
|
|
857
|
+
const taskId = readString(entry.payload?.taskId);
|
|
858
|
+
if (!taskId || seenTaskIds.has(taskId)) continue;
|
|
859
|
+
seenTaskIds.add(taskId);
|
|
860
|
+
candidates.push({
|
|
861
|
+
taskId,
|
|
862
|
+
nodeId: entry.nodeId,
|
|
863
|
+
sessionId: entry.sessionId,
|
|
864
|
+
providerType: entry.providerType || readString(entry.payload?.providerType),
|
|
865
|
+
message: readString(entry.payload?.message),
|
|
866
|
+
dispatchedAt: entry.timestamp,
|
|
867
|
+
via: readString(entry.payload?.via)
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
return candidates;
|
|
871
|
+
}
|
|
872
|
+
async function reconcileDirectDispatchesFromTranscriptEvidence(ctx, liveNodes, directDispatches, ledgerEntries) {
|
|
873
|
+
let attempted = 0;
|
|
874
|
+
let reconciled = 0;
|
|
875
|
+
let skipped = 0;
|
|
876
|
+
const candidates = buildDirectDispatchReconciliationCandidates(directDispatches, ledgerEntries);
|
|
877
|
+
for (const dispatch of candidates) {
|
|
878
|
+
const taskId = readString(dispatch?.taskId);
|
|
879
|
+
const nodeId = readString(dispatch?.nodeId);
|
|
880
|
+
const sessionId = readString(dispatch?.sessionId);
|
|
881
|
+
if (!taskId || !nodeId || !sessionId) {
|
|
882
|
+
skipped += 1;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const { session } = findNodeSession(liveNodes, nodeId, sessionId);
|
|
886
|
+
if (!session || !isIdleSessionRecord(session)) {
|
|
887
|
+
skipped += 1;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const node = await findOptionalNodeWithRefresh(ctx, nodeId).catch(() => null);
|
|
891
|
+
if (!node) {
|
|
892
|
+
skipped += 1;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const providerType = readString(dispatch?.providerType) || resolveSessionProviderType(session);
|
|
896
|
+
const providerSessionId = readString(session?.providerSessionId) || readString(session?.activeChat?.providerSessionId) || readString(session?.settings?.providerSessionId) || resolveMeshSessionProviderMetadata(ctx, nodeId, sessionId)?.providerSessionId;
|
|
897
|
+
attempted += 1;
|
|
898
|
+
try {
|
|
899
|
+
const readResult = await commandForNode(ctx, node, "read_chat", {
|
|
900
|
+
sessionId,
|
|
901
|
+
targetSessionId: sessionId,
|
|
902
|
+
workspace: node.workspace,
|
|
903
|
+
...providerType ? { agentType: providerType, providerType } : {},
|
|
904
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
905
|
+
tailLimit: 10
|
|
906
|
+
});
|
|
907
|
+
const payload = unwrapCommandPayload(readResult);
|
|
908
|
+
if (payload?.success === false) continue;
|
|
909
|
+
const evidence = readFinalAssistantTranscriptEvidence(payload);
|
|
910
|
+
if (!evidence.finalSummary) continue;
|
|
911
|
+
const result = (0, import_daemon_core.reconcileDirectDispatchCompletionFromTranscript)({
|
|
912
|
+
meshId: ctx.mesh.id,
|
|
913
|
+
nodeId,
|
|
914
|
+
sessionId,
|
|
915
|
+
providerType,
|
|
916
|
+
providerSessionId: readString(payload?.providerSessionId) || providerSessionId,
|
|
917
|
+
taskId,
|
|
918
|
+
finalSummary: evidence.finalSummary,
|
|
919
|
+
transcriptMessageAt: evidence.transcriptMessageAt,
|
|
920
|
+
targetCoordinatorDaemonId: ctx.localDaemonId,
|
|
921
|
+
source: "mcp_mesh_status_transcript_reconciliation"
|
|
922
|
+
});
|
|
923
|
+
if (result.reconciled) reconciled += 1;
|
|
924
|
+
} catch {
|
|
925
|
+
skipped += 1;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return { attempted, reconciled, skipped };
|
|
929
|
+
}
|
|
930
|
+
async function triggerMeshQueueAndReport(ctx, node, opts) {
|
|
931
|
+
try {
|
|
932
|
+
let raw;
|
|
933
|
+
if (ctx.transport instanceof IpcTransport && node?.daemonId && opts?.localNode === false) {
|
|
934
|
+
raw = await ctx.transport.meshCommand(node.daemonId, "trigger_mesh_queue", { meshId: ctx.mesh.id });
|
|
935
|
+
} else {
|
|
936
|
+
raw = await ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id });
|
|
937
|
+
}
|
|
938
|
+
const payload = unwrapCommandPayload(raw);
|
|
939
|
+
const trigger = payload?.trigger && typeof payload.trigger === "object" ? payload.trigger : payload;
|
|
940
|
+
return trigger && typeof trigger === "object" ? trigger : { success: true };
|
|
941
|
+
} catch (e) {
|
|
942
|
+
return {
|
|
943
|
+
success: false,
|
|
944
|
+
error: e?.message || String(e)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function buildQueueTriggerGuidance(queueTrigger) {
|
|
949
|
+
if (!queueTrigger || queueTrigger.claimed === true) return void 0;
|
|
950
|
+
if (queueTrigger.success === false) {
|
|
951
|
+
return {
|
|
952
|
+
queueClaimed: false,
|
|
953
|
+
queueDispatchState: "trigger_failed",
|
|
954
|
+
nextAction: "Do not assume the queued task is running. Check mesh_view_queue and daemon connectivity before redispatching."
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
if (queueTrigger.noIdleMeshSessionAvailable === true) {
|
|
958
|
+
return {
|
|
959
|
+
queueClaimed: false,
|
|
960
|
+
queueDispatchState: "pending_no_idle_mesh_session",
|
|
961
|
+
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."
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
queueClaimed: false,
|
|
966
|
+
queueDispatchState: "pending_or_waiting_for_ready",
|
|
967
|
+
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."
|
|
968
|
+
};
|
|
969
|
+
}
|
|
615
970
|
function isTerminalSessionRecord(session) {
|
|
616
971
|
const status = typeof session?.status === "string" ? session.status.toLowerCase() : "";
|
|
617
972
|
const lifecycle = typeof session?.lifecycle === "string" ? session.lifecycle.toLowerCase() : "";
|
|
@@ -627,16 +982,23 @@ function isIdleSessionRecord(session) {
|
|
|
627
982
|
function isMeshOwnedDelegateSession(session, meshId, nodeId) {
|
|
628
983
|
const settings = session?.settings;
|
|
629
984
|
const sessionMeshId = typeof settings?.meshNodeFor === "string" ? settings.meshNodeFor.trim() : "";
|
|
630
|
-
const coordinatorDaemonId = typeof settings?.meshCoordinatorDaemonId === "string" ? settings.meshCoordinatorDaemonId.trim() : "";
|
|
631
985
|
const sessionNodeId = typeof settings?.meshNodeId === "string" ? settings.meshNodeId.trim() : "";
|
|
632
|
-
if (sessionMeshId !== meshId
|
|
986
|
+
if (sessionMeshId !== meshId) return false;
|
|
633
987
|
return !sessionNodeId || sessionNodeId === nodeId;
|
|
634
988
|
}
|
|
989
|
+
function hasRemoteRelayMetadata(session) {
|
|
990
|
+
return Boolean(
|
|
991
|
+
readString(session?.settings?.meshCoordinatorDaemonId) || readString(session?.meta?.meshCoordinatorDaemonId) || readString(session?.metadata?.meshCoordinatorDaemonId) || readString(session?.meshCoordinatorDaemonId)
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
function isRelaySafeRemoteDelegateSession(session, meshId, nodeId) {
|
|
995
|
+
return isMeshOwnedDelegateSession(session, meshId, nodeId) && hasRemoteRelayMetadata(session);
|
|
996
|
+
}
|
|
635
997
|
function chooseDispatchableSession(sessions, providerType, meshId, nodeId) {
|
|
636
998
|
const live = sessions.filter((session) => !isTerminalSessionRecord(session));
|
|
637
999
|
const matchingProvider = (session) => !providerType || session?.providerType === providerType || session?.cliType === providerType;
|
|
638
1000
|
const meshSessions = live.filter(
|
|
639
|
-
(session) =>
|
|
1001
|
+
(session) => isRelaySafeRemoteDelegateSession(session, meshId, nodeId)
|
|
640
1002
|
);
|
|
641
1003
|
return meshSessions.find((session) => isIdleSessionRecord(session) && matchingProvider(session)) || meshSessions.find(matchingProvider) || void 0;
|
|
642
1004
|
}
|
|
@@ -653,8 +1015,9 @@ function buildRelayUnsafeRemoteSessionFailure(ctx, node, sessionId, providerType
|
|
|
653
1015
|
daemonId: node.daemonId,
|
|
654
1016
|
workspace: node.workspace,
|
|
655
1017
|
sessionId,
|
|
1018
|
+
unsafeTranscriptAlias: true,
|
|
656
1019
|
...providerType ? { resolvedProviderType: providerType } : {},
|
|
657
|
-
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.`,
|
|
1020
|
+
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).`,
|
|
658
1021
|
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.`,
|
|
659
1022
|
noFallbackReason: "Blindly reusing a remote session without mesh relay metadata would silently drop task_completed / generating_completed events."
|
|
660
1023
|
};
|
|
@@ -704,12 +1067,16 @@ function extractGitDiff(value) {
|
|
|
704
1067
|
}
|
|
705
1068
|
function extractSubmodules(value, ignorePaths) {
|
|
706
1069
|
const payload = unwrapCommandPayload(value);
|
|
707
|
-
const subs = payload?.submodules ?? value?.submodules;
|
|
1070
|
+
const subs = payload?.status?.submodules ?? payload?.submodules ?? value?.status?.submodules ?? value?.submodules;
|
|
708
1071
|
if (!Array.isArray(subs)) return void 0;
|
|
709
1072
|
if (ignorePaths.length === 0) return subs;
|
|
710
1073
|
const ignoreSet = new Set(ignorePaths);
|
|
711
1074
|
return subs.filter((s) => s?.path && !ignoreSet.has(s.path));
|
|
712
1075
|
}
|
|
1076
|
+
function assignFullGitSnapshot(entry, status) {
|
|
1077
|
+
if (!status || typeof status !== "object" || Array.isArray(status)) return;
|
|
1078
|
+
entry.git = status;
|
|
1079
|
+
}
|
|
713
1080
|
function extractLaunchPayload(value) {
|
|
714
1081
|
return findNestedPayload(value, (payload) => Boolean(payload?.sessionId || payload?.id || payload?.runtimeSessionId));
|
|
715
1082
|
}
|
|
@@ -834,7 +1201,20 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
834
1201
|
let sessionId = args.session_id?.trim() || "";
|
|
835
1202
|
const providerPriorityList = Array.isArray(node.policy?.providerPriority) ? node.policy.providerPriority : [];
|
|
836
1203
|
let resolvedProviderType = args.providerType?.trim() || providerPriorityList[0] || "";
|
|
837
|
-
if (
|
|
1204
|
+
if (sessionId && args.verifiedSession) {
|
|
1205
|
+
const explicitSession = args.verifiedSession;
|
|
1206
|
+
if (!isRelaySafeRemoteDelegateSession(explicitSession, ctx.mesh.id, node.id)) {
|
|
1207
|
+
return buildRelayUnsafeRemoteSessionFailure(
|
|
1208
|
+
ctx,
|
|
1209
|
+
node,
|
|
1210
|
+
sessionId,
|
|
1211
|
+
resolvedProviderType || resolveSessionProviderType(explicitSession) || void 0
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
if (!resolvedProviderType) {
|
|
1215
|
+
resolvedProviderType = resolveSessionProviderType(explicitSession);
|
|
1216
|
+
}
|
|
1217
|
+
} else if (!sessionId || args.session_id) {
|
|
838
1218
|
try {
|
|
839
1219
|
const relayResult = await transport.meshCommand(daemonId, "get_status_metadata", {});
|
|
840
1220
|
const sessions = extractStatusMetadataSessions(relayResult);
|
|
@@ -858,7 +1238,7 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
858
1238
|
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.`
|
|
859
1239
|
};
|
|
860
1240
|
}
|
|
861
|
-
if (!
|
|
1241
|
+
if (!isRelaySafeRemoteDelegateSession(explicitSession, ctx.mesh.id, node.id)) {
|
|
862
1242
|
return buildRelayUnsafeRemoteSessionFailure(
|
|
863
1243
|
ctx,
|
|
864
1244
|
node,
|
|
@@ -902,7 +1282,8 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
902
1282
|
agentType: resolvedProviderType,
|
|
903
1283
|
cliType: resolvedProviderType,
|
|
904
1284
|
action: "send_chat",
|
|
905
|
-
message: args.message
|
|
1285
|
+
message: args.message,
|
|
1286
|
+
...args.meshContext ? { meshContext: args.meshContext } : {}
|
|
906
1287
|
});
|
|
907
1288
|
const dispatchPayload = unwrapCommandPayload(dispatchResult);
|
|
908
1289
|
if (dispatchPayload?.success === false || dispatchResult?.success === false) {
|
|
@@ -920,7 +1301,7 @@ async function ipcDispatchToRemoteAgent(ctx, node, args) {
|
|
|
920
1301
|
error: `P2P dispatch failed: ${errorMessage}`
|
|
921
1302
|
};
|
|
922
1303
|
}
|
|
923
|
-
return { success: true, dispatched: true, sessionId: sessionId || resolvedProviderType };
|
|
1304
|
+
return { success: true, dispatched: true, sessionId: sessionId || resolvedProviderType, providerType: resolvedProviderType };
|
|
924
1305
|
} catch (e) {
|
|
925
1306
|
const errorMessage = e?.message || String(e);
|
|
926
1307
|
return {
|
|
@@ -950,34 +1331,197 @@ function resolveCoordinatorNode(ctx) {
|
|
|
950
1331
|
return void 0;
|
|
951
1332
|
}
|
|
952
1333
|
function readNodeMachineId(node) {
|
|
953
|
-
return readString(node.machineId) || readString(node.machine_id);
|
|
1334
|
+
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);
|
|
954
1335
|
}
|
|
955
1336
|
function readNodeDaemonId(node) {
|
|
956
|
-
return readString(node.daemonId) || readString(node.daemon_id);
|
|
1337
|
+
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);
|
|
1338
|
+
}
|
|
1339
|
+
function normalizeHostname(value) {
|
|
1340
|
+
const hostname = readString(value);
|
|
1341
|
+
if (!hostname) return void 0;
|
|
1342
|
+
return hostname.toLowerCase().replace(/\.$/, "");
|
|
1343
|
+
}
|
|
1344
|
+
function readNodeHostname(node) {
|
|
1345
|
+
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);
|
|
1346
|
+
}
|
|
1347
|
+
function readNodeDisplayMachineName(node) {
|
|
1348
|
+
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);
|
|
1349
|
+
}
|
|
1350
|
+
function compactIdentityEvidence(value) {
|
|
1351
|
+
if (!value) return void 0;
|
|
1352
|
+
return value.length > 24 ? `${value.slice(0, 12)}\u2026${value.slice(-8)}` : value;
|
|
1353
|
+
}
|
|
1354
|
+
function pushIdentityEvidence(evidence, label, value) {
|
|
1355
|
+
const compact = compactIdentityEvidence(value);
|
|
1356
|
+
if (compact) evidence.push(`${label}:${compact}`);
|
|
1357
|
+
}
|
|
1358
|
+
function buildNodeMachineIdentity(ctx, node) {
|
|
1359
|
+
const machineId = readNodeMachineId(node);
|
|
1360
|
+
const daemonId = readNodeDaemonId(node);
|
|
1361
|
+
const hostname = readNodeHostname(node);
|
|
1362
|
+
const machineName = readNodeDisplayMachineName(node);
|
|
1363
|
+
const coordinatorHostname = readString(ctx.coordinatorHostname);
|
|
1364
|
+
const localControlPlaneReason = getLocalControlPlaneMatchReason(ctx, node);
|
|
1365
|
+
const directLocal = !!localControlPlaneReason;
|
|
1366
|
+
const hostnameMatches = Boolean(
|
|
1367
|
+
normalizeHostname(hostname) && normalizeHostname(coordinatorHostname) && normalizeHostname(hostname) === normalizeHostname(coordinatorHostname)
|
|
1368
|
+
);
|
|
1369
|
+
const sameMachine = directLocal || hostnameMatches;
|
|
1370
|
+
const evidence = [];
|
|
1371
|
+
pushIdentityEvidence(evidence, "machineName", machineName);
|
|
1372
|
+
pushIdentityEvidence(evidence, "hostname", hostname);
|
|
1373
|
+
pushIdentityEvidence(evidence, "machineId", machineId);
|
|
1374
|
+
pushIdentityEvidence(evidence, "daemonId", daemonId);
|
|
1375
|
+
if (localControlPlaneReason) {
|
|
1376
|
+
pushIdentityEvidence(evidence, "localMatch", localControlPlaneReason);
|
|
1377
|
+
pushIdentityEvidence(evidence, "localMachineId", ctx.localMachineId);
|
|
1378
|
+
pushIdentityEvidence(evidence, "localDaemonId", ctx.localDaemonId);
|
|
1379
|
+
}
|
|
1380
|
+
const locality = sameMachine ? "same_machine" : evidence.length > 0 ? "remote_known" : "remote_or_unknown";
|
|
1381
|
+
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";
|
|
1382
|
+
return {
|
|
1383
|
+
daemonId,
|
|
1384
|
+
machineId,
|
|
1385
|
+
hostname,
|
|
1386
|
+
machineName,
|
|
1387
|
+
displayName: machineName || hostname || daemonId || machineId,
|
|
1388
|
+
coordinatorHostname,
|
|
1389
|
+
sameMachine,
|
|
1390
|
+
locality,
|
|
1391
|
+
localityReason,
|
|
1392
|
+
identityEvidence: evidence
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
function nodeHasLocalDaemonEvidence(ctx, node) {
|
|
1396
|
+
const isLocal = (session) => {
|
|
1397
|
+
if (!session || typeof session !== "object") return false;
|
|
1398
|
+
if (ctx.localDaemonId && session.runtime?.owner === ctx.localDaemonId) return true;
|
|
1399
|
+
if (ctx.localDaemonId && session.daemonClient?.daemonId === ctx.localDaemonId) return true;
|
|
1400
|
+
return false;
|
|
1401
|
+
};
|
|
1402
|
+
const sessionArrays = [
|
|
1403
|
+
node?.sessions,
|
|
1404
|
+
node?.activeSessions,
|
|
1405
|
+
node?.active_sessions,
|
|
1406
|
+
node?.lastProbe?.sessions,
|
|
1407
|
+
node?.last_probe?.sessions,
|
|
1408
|
+
node?.lastProbe?.status?.sessions,
|
|
1409
|
+
node?.last_probe?.status?.sessions
|
|
1410
|
+
];
|
|
1411
|
+
for (const arr of sessionArrays) {
|
|
1412
|
+
if (Array.isArray(arr) && arr.some(isLocal)) return true;
|
|
1413
|
+
}
|
|
1414
|
+
const sessionRecords = [
|
|
1415
|
+
node?.activeSession,
|
|
1416
|
+
node?.active_session,
|
|
1417
|
+
node?.currentSession,
|
|
1418
|
+
node?.current_session,
|
|
1419
|
+
node?.runtimeSession,
|
|
1420
|
+
node?.runtime_session,
|
|
1421
|
+
node?.session,
|
|
1422
|
+
node?.lastProbe?.activeSession,
|
|
1423
|
+
node?.last_probe?.active_session,
|
|
1424
|
+
node?.lastProbe?.currentSession,
|
|
1425
|
+
node?.last_probe?.current_session,
|
|
1426
|
+
node?.lastProbe?.session,
|
|
1427
|
+
node?.last_probe?.session
|
|
1428
|
+
];
|
|
1429
|
+
for (const session of sessionRecords) {
|
|
1430
|
+
if (isLocal(session)) return true;
|
|
1431
|
+
}
|
|
1432
|
+
return false;
|
|
957
1433
|
}
|
|
958
1434
|
function isDirectLocalNode(ctx, node) {
|
|
959
1435
|
const machineId = readNodeMachineId(node);
|
|
960
1436
|
const daemonId = readNodeDaemonId(node);
|
|
961
1437
|
return Boolean(
|
|
962
|
-
ctx.localMachineId && machineId === ctx.localMachineId || ctx.localDaemonId && daemonId === ctx.localDaemonId
|
|
1438
|
+
ctx.localMachineId && machineId === ctx.localMachineId || ctx.localDaemonId && daemonId === ctx.localDaemonId || nodeHasLocalDaemonEvidence(ctx, node)
|
|
963
1439
|
);
|
|
964
1440
|
}
|
|
1441
|
+
function isConfiguredCoordinatorNode(ctx, node) {
|
|
1442
|
+
if (!ctx.localMachineId && !ctx.localDaemonId) return false;
|
|
1443
|
+
const nodeId = readString(node.id) || readString(node.nodeId) || readString(node.node_id);
|
|
1444
|
+
if (!nodeId) return false;
|
|
1445
|
+
const nodeDaemonId = readNodeDaemonId(node);
|
|
1446
|
+
const nodeMachineId = readNodeMachineId(node);
|
|
1447
|
+
if (nodeDaemonId && ctx.localDaemonId && nodeDaemonId !== ctx.localDaemonId) return false;
|
|
1448
|
+
if (nodeMachineId && ctx.localMachineId && nodeMachineId !== ctx.localMachineId) return false;
|
|
1449
|
+
const preferredNodeId = readString(ctx.mesh.coordinator?.preferredNodeId) || readString(ctx.mesh.coordinator?.preferred_node_id);
|
|
1450
|
+
if (preferredNodeId) return nodeId === preferredNodeId;
|
|
1451
|
+
const first = ctx.mesh.nodes?.[0];
|
|
1452
|
+
const firstNodeId = readString(first?.id) || readString(first?.nodeId) || readString(first?.node_id);
|
|
1453
|
+
return !!firstNodeId && nodeId === firstNodeId;
|
|
1454
|
+
}
|
|
1455
|
+
function getLocalControlPlaneMatchReason(ctx, node) {
|
|
1456
|
+
if (isDirectLocalNode(ctx, node)) return "matched coordinator daemon or machine id";
|
|
1457
|
+
if (isConfiguredCoordinatorNode(ctx, node)) return "matched configured coordinator node";
|
|
1458
|
+
if (node.isLocalWorktree === true) {
|
|
1459
|
+
const sourceNode = findClonedFromNode(ctx, node);
|
|
1460
|
+
if (sourceNode && isDirectLocalNode(ctx, sourceNode)) return "matched local cloned-from node";
|
|
1461
|
+
if (sourceNode && isConfiguredCoordinatorNode(ctx, sourceNode)) return "matched configured coordinator source node";
|
|
1462
|
+
}
|
|
1463
|
+
return void 0;
|
|
1464
|
+
}
|
|
965
1465
|
function findClonedFromNode(ctx, node) {
|
|
966
1466
|
const clonedFromNodeId = readString(node.clonedFromNodeId) || readString(node.cloned_from_node_id);
|
|
967
1467
|
if (!clonedFromNodeId) return void 0;
|
|
968
1468
|
return ctx.mesh.nodes.find((n) => n.id === clonedFromNodeId || n.nodeId === clonedFromNodeId || n.node_id === clonedFromNodeId);
|
|
969
1469
|
}
|
|
970
1470
|
function isLocalControlPlaneNode(ctx, node) {
|
|
971
|
-
|
|
972
|
-
if (node.isLocalWorktree === true) {
|
|
973
|
-
const sourceNode = findClonedFromNode(ctx, node);
|
|
974
|
-
if (sourceNode && isDirectLocalNode(ctx, sourceNode)) return true;
|
|
975
|
-
}
|
|
976
|
-
return false;
|
|
1471
|
+
return !!getLocalControlPlaneMatchReason(ctx, node);
|
|
977
1472
|
}
|
|
978
1473
|
function meshSessionCacheKey(nodeId, runtimeSessionId) {
|
|
979
1474
|
return `${nodeId}:${runtimeSessionId}`;
|
|
980
1475
|
}
|
|
1476
|
+
function rememberMeshSessionProviderMetadata(nodeId, runtimeSessionId, metadata) {
|
|
1477
|
+
const keyNodeId = readString(nodeId);
|
|
1478
|
+
const keySessionId = readString(runtimeSessionId);
|
|
1479
|
+
if (!keyNodeId || !keySessionId) return;
|
|
1480
|
+
const providerType = readString(metadata.providerType);
|
|
1481
|
+
const providerSessionId = readString(metadata.providerSessionId);
|
|
1482
|
+
if (!providerType && !providerSessionId) return;
|
|
1483
|
+
const existing = getSessionMetadata(meshSessionCacheKey(keyNodeId, keySessionId)) || { providerType: "" };
|
|
1484
|
+
meshSessionProviderMetadata.set(meshSessionCacheKey(keyNodeId, keySessionId), {
|
|
1485
|
+
providerType: providerType || existing.providerType,
|
|
1486
|
+
providerSessionId: providerSessionId || existing.providerSessionId,
|
|
1487
|
+
expiresAt: Date.now() + SESSION_PROVIDER_METADATA_TTL_MS
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
function rememberMeshSessionProviderMetadataFromEvent(event) {
|
|
1491
|
+
const metadataEvent = event?.metadataEvent && typeof event.metadataEvent === "object" ? event.metadataEvent : event && typeof event === "object" ? event : {};
|
|
1492
|
+
const nodeId = readString(event?.nodeId) || readString(metadataEvent.nodeId) || readString(metadataEvent.meshNodeId);
|
|
1493
|
+
const sessionId = readString(metadataEvent.targetSessionId) || readString(metadataEvent.sessionId) || readString(metadataEvent.instanceId) || readString(event?.sessionId);
|
|
1494
|
+
rememberMeshSessionProviderMetadata(nodeId, sessionId, {
|
|
1495
|
+
providerType: readString(metadataEvent.providerType) || readString(event?.providerType) || "",
|
|
1496
|
+
providerSessionId: readString(metadataEvent.providerSessionId) || readString(event?.providerSessionId)
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
function resolveMeshSessionProviderMetadataFromLedger(ctx, nodeId, runtimeSessionId) {
|
|
1500
|
+
const entries = (0, import_daemon_core.readLedgerEntries)(ctx.mesh.id, { tail: 50 });
|
|
1501
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1502
|
+
const entry = entries[i];
|
|
1503
|
+
const payload = entry.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload) ? entry.payload : {};
|
|
1504
|
+
const entryNodeId = readString(entry.nodeId) || readString(payload.nodeId) || readString(payload.meshNodeId);
|
|
1505
|
+
if (entryNodeId && entryNodeId !== nodeId) continue;
|
|
1506
|
+
const entrySessionId = readString(entry.sessionId) || readString(payload.targetSessionId) || readString(payload.sessionId) || readString(payload.instanceId);
|
|
1507
|
+
if (entrySessionId !== runtimeSessionId) continue;
|
|
1508
|
+
const providerType = readString(entry.providerType) || readString(payload.providerType);
|
|
1509
|
+
const completionDiagnostic = payload.completionDiagnostic && typeof payload.completionDiagnostic === "object" && !Array.isArray(payload.completionDiagnostic) ? payload.completionDiagnostic : {};
|
|
1510
|
+
const metadataEvent = payload.metadataEvent && typeof payload.metadataEvent === "object" && !Array.isArray(payload.metadataEvent) ? payload.metadataEvent : {};
|
|
1511
|
+
const providerSessionId = readString(payload.providerSessionId) || readString(completionDiagnostic.providerSessionId) || readString(metadataEvent.providerSessionId);
|
|
1512
|
+
if (providerType || providerSessionId) {
|
|
1513
|
+
return { providerType: providerType || "", providerSessionId };
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return void 0;
|
|
1517
|
+
}
|
|
1518
|
+
function resolveMeshSessionProviderMetadata(ctx, nodeId, runtimeSessionId) {
|
|
1519
|
+
const cached = getSessionMetadata(meshSessionCacheKey(nodeId, runtimeSessionId));
|
|
1520
|
+
if (cached?.providerType || cached?.providerSessionId) return cached;
|
|
1521
|
+
const fromLedger = resolveMeshSessionProviderMetadataFromLedger(ctx, nodeId, runtimeSessionId);
|
|
1522
|
+
if (fromLedger) rememberMeshSessionProviderMetadata(nodeId, runtimeSessionId, fromLedger);
|
|
1523
|
+
return fromLedger;
|
|
1524
|
+
}
|
|
981
1525
|
function countUncommittedChanges(status) {
|
|
982
1526
|
if (typeof status?.uncommittedChanges === "number") return status.uncommittedChanges;
|
|
983
1527
|
const keys = ["staged", "modified", "untracked", "deleted", "renamed"];
|
|
@@ -988,8 +1532,23 @@ function countUncommittedChanges(status) {
|
|
|
988
1532
|
function isGitStatusDirty(status) {
|
|
989
1533
|
if (typeof status?.isDirty === "boolean") return status.isDirty;
|
|
990
1534
|
if (typeof status?.dirty === "boolean") return status.dirty;
|
|
1535
|
+
if (Array.isArray(status?.submodules) && status.submodules.some((submodule) => submodule?.dirty || submodule?.outOfSync || submodule?.error)) return true;
|
|
991
1536
|
return countUncommittedChanges(status) > 0;
|
|
992
1537
|
}
|
|
1538
|
+
function slimLedgerPayload(payload) {
|
|
1539
|
+
const slim = {};
|
|
1540
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
1541
|
+
if (k === "message" || k === "taskSummary") {
|
|
1542
|
+
slim[k] = typeof v === "string" && v.length > 200 ? v.slice(0, 200) + "\u2026" : v;
|
|
1543
|
+
} else if (k === "evidence" || k === "workerResult" || k === "gitStatus" || k === "validationResults") {
|
|
1544
|
+
} else if (k === "finalSummary") {
|
|
1545
|
+
slim[k] = typeof v === "string" && v.length > 300 ? v.slice(0, 300) + "\u2026" : v;
|
|
1546
|
+
} else {
|
|
1547
|
+
slim[k] = v;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return slim;
|
|
1551
|
+
}
|
|
993
1552
|
function readRelatedRepos(node) {
|
|
994
1553
|
const raw = Array.isArray(node.relatedRepos) ? node.relatedRepos : Array.isArray(node.policy?.relatedRepos) ? node.policy.relatedRepos : [];
|
|
995
1554
|
return raw.map((entry) => ({
|
|
@@ -1024,7 +1583,7 @@ async function collectRelatedRepoStatuses(ctx, node) {
|
|
|
1024
1583
|
const results = [];
|
|
1025
1584
|
for (const repo of relatedRepos) {
|
|
1026
1585
|
try {
|
|
1027
|
-
const statusResult =
|
|
1586
|
+
const statusResult = await commandForNode(ctx, node, "git_status", { workspace: repo.workspace, refreshUpstream: true });
|
|
1028
1587
|
const status = extractGitStatus(statusResult);
|
|
1029
1588
|
results.push(summarizeRelatedRepoStatus(repo, status));
|
|
1030
1589
|
} catch (e) {
|
|
@@ -1048,6 +1607,16 @@ function missingProviderPriorityMessage(nodeId) {
|
|
|
1048
1607
|
return `Node '${nodeId}' has no providerPriority policy; pass type explicitly or configure node.policy.providerPriority`;
|
|
1049
1608
|
}
|
|
1050
1609
|
function getNodeLaunchReadiness(node) {
|
|
1610
|
+
const bootstrap = node.worktreeBootstrap;
|
|
1611
|
+
if (node.isLocalWorktree && bootstrap?.status === "failed" && bootstrap?.required !== false) {
|
|
1612
|
+
return {
|
|
1613
|
+
providerPriority: readProviderPriority(node.policy),
|
|
1614
|
+
launchReady: false,
|
|
1615
|
+
launchBlockedReason: "worktree_bootstrap_failed",
|
|
1616
|
+
launchBlockedMessage: typeof bootstrap.error === "string" && bootstrap.error.trim() ? bootstrap.error.trim() : "Required worktree bootstrap failed; resolve it before launching an agent into this node.",
|
|
1617
|
+
worktreeBootstrap: bootstrap
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1051
1620
|
const providerPriority = readProviderPriority(node.policy);
|
|
1052
1621
|
if (providerPriority.length) {
|
|
1053
1622
|
return {
|
|
@@ -1062,6 +1631,45 @@ function getNodeLaunchReadiness(node) {
|
|
|
1062
1631
|
launchBlockedMessage: missingProviderPriorityMessage(node.id)
|
|
1063
1632
|
};
|
|
1064
1633
|
}
|
|
1634
|
+
function getWorktreeBootstrapLaunchBlock(node, meshPolicy) {
|
|
1635
|
+
if (!node.isLocalWorktree) return void 0;
|
|
1636
|
+
const bootstrap = node.worktreeBootstrap;
|
|
1637
|
+
const requireReady = !!(meshPolicy && typeof meshPolicy === "object" && meshPolicy.requireBootstrapBeforeLaunch === true);
|
|
1638
|
+
if (requireReady && bootstrap?.status !== "ready") {
|
|
1639
|
+
return {
|
|
1640
|
+
success: false,
|
|
1641
|
+
code: "bootstrap_not_ready",
|
|
1642
|
+
error: `Node '${node.id}' bootstrap state is '${bootstrap?.status ?? "unknown"}' and mesh policy requireBootstrapBeforeLaunch is enabled.`,
|
|
1643
|
+
nodeId: node.id,
|
|
1644
|
+
worktreeBootstrap: bootstrap ?? null,
|
|
1645
|
+
recoveryHint: "Run the worktree bootstrap (clone runOnClone or a refine with bootstrap inherit) until the node reports ready, or disable requireBootstrapBeforeLaunch."
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
if (bootstrap?.status !== "failed" || bootstrap?.required === false) return void 0;
|
|
1649
|
+
return {
|
|
1650
|
+
success: false,
|
|
1651
|
+
code: "worktree_bootstrap_failed",
|
|
1652
|
+
error: typeof bootstrap.error === "string" && bootstrap.error.trim() ? bootstrap.error.trim() : `Node '${node.id}' has a failed required worktree bootstrap.`,
|
|
1653
|
+
nodeId: node.id,
|
|
1654
|
+
worktreeBootstrap: bootstrap,
|
|
1655
|
+
recoveryHint: "Fix the configured worktree bootstrap command or remove/recreate the worktree node before launching an agent."
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
async function collectLiveStatusSessions(ctx, node) {
|
|
1659
|
+
try {
|
|
1660
|
+
const statusResult = await commandForNode(ctx, node, "get_status_metadata", {});
|
|
1661
|
+
return extractStatusMetadataSessions(statusResult);
|
|
1662
|
+
} catch {
|
|
1663
|
+
return [];
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
async function collectMeshViewQueueNodesWithLiveSessions(ctx) {
|
|
1667
|
+
const nodes = await Promise.all(ctx.mesh.nodes.map(async (node) => {
|
|
1668
|
+
const liveSessions = await collectLiveStatusSessions(ctx, node);
|
|
1669
|
+
return liveSessions.length > 0 ? { ...node, sessions: liveSessions } : node;
|
|
1670
|
+
}));
|
|
1671
|
+
return nodes;
|
|
1672
|
+
}
|
|
1065
1673
|
function readNumeric(value, fallback = 0) {
|
|
1066
1674
|
const parsed = Number(value);
|
|
1067
1675
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -1194,10 +1802,7 @@ async function commandForNode(ctx, node, command, args = {}) {
|
|
|
1194
1802
|
if (ctx.transport instanceof IpcTransport && node.daemonId && !isLocalNode) {
|
|
1195
1803
|
return ctx.transport.meshCommand(node.daemonId, command, args);
|
|
1196
1804
|
}
|
|
1197
|
-
|
|
1198
|
-
return ctx.transport.command(command, args);
|
|
1199
|
-
}
|
|
1200
|
-
throw new Error(`Command '${command}' requires daemon IPC/local transport for node '${node.id}'`);
|
|
1805
|
+
return ctx.transport.command(command, args);
|
|
1201
1806
|
}
|
|
1202
1807
|
function normalizePendingMeshCoordinatorEvents(value) {
|
|
1203
1808
|
const payload = unwrapCommandPayload(value);
|
|
@@ -1215,6 +1820,14 @@ function buildMeshForwardPayloadFromPendingEvent(event) {
|
|
|
1215
1820
|
providerType: readString(metadataEvent.providerType),
|
|
1216
1821
|
providerSessionId: readString(metadataEvent.providerSessionId),
|
|
1217
1822
|
finalSummary: readString(metadataEvent.finalSummary) || readString(metadataEvent.summary),
|
|
1823
|
+
jobId: readString(metadataEvent.jobId),
|
|
1824
|
+
interactionId: readString(metadataEvent.interactionId),
|
|
1825
|
+
status: readString(metadataEvent.status),
|
|
1826
|
+
targetDaemonId: readString(metadataEvent.targetDaemonId),
|
|
1827
|
+
startedAt: readString(metadataEvent.startedAt),
|
|
1828
|
+
completedAt: readString(metadataEvent.completedAt),
|
|
1829
|
+
retryOfJobId: readString(metadataEvent.retryOfJobId),
|
|
1830
|
+
...metadataEvent.result && typeof metadataEvent.result === "object" && !Array.isArray(metadataEvent.result) ? { result: metadataEvent.result } : {},
|
|
1218
1831
|
...metadataEvent.intentional === true ? { intentional: true } : {},
|
|
1219
1832
|
...metadataEvent.intentionalStop === true ? { intentionalStop: true } : {},
|
|
1220
1833
|
...metadataEvent.operatorCleanup === true ? { operatorCleanup: true } : {},
|
|
@@ -1229,10 +1842,25 @@ async function drainCoordinatorPendingEvents(ctx, opts) {
|
|
|
1229
1842
|
const matchesCurrentMesh = (event) => readString(event?.meshId) === ctx.mesh.id;
|
|
1230
1843
|
if (ctx.transport instanceof IpcTransport) {
|
|
1231
1844
|
const surfacedEvents = [];
|
|
1845
|
+
const coordinatorDaemonId = readString(ctx.localDaemonId);
|
|
1846
|
+
const pendingEventArgs = {
|
|
1847
|
+
meshId: ctx.mesh.id,
|
|
1848
|
+
...coordinatorDaemonId ? { coordinatorDaemonId } : {}
|
|
1849
|
+
};
|
|
1232
1850
|
try {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1851
|
+
const localEvents = normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", pendingEventArgs)).filter(matchesCurrentMesh);
|
|
1852
|
+
for (const event of localEvents) {
|
|
1853
|
+
const payload = buildMeshForwardPayloadFromPendingEvent(event);
|
|
1854
|
+
if (!payload.event || !payload.meshId) continue;
|
|
1855
|
+
let injected = false;
|
|
1856
|
+
try {
|
|
1857
|
+
await ctx.transport.command("mesh_forward_event", payload);
|
|
1858
|
+
injected = true;
|
|
1859
|
+
} catch {
|
|
1860
|
+
}
|
|
1861
|
+
rememberMeshSessionProviderMetadataFromEvent({ ...event, metadataEvent: payload });
|
|
1862
|
+
if (!injected) surfacedEvents.push(event);
|
|
1863
|
+
}
|
|
1236
1864
|
} catch {
|
|
1237
1865
|
}
|
|
1238
1866
|
for (const node of ctx.mesh.nodes) {
|
|
@@ -1240,29 +1868,39 @@ async function drainCoordinatorPendingEvents(ctx, opts) {
|
|
|
1240
1868
|
if (requestedNodeIds && !requestedNodeIds.has(node.id)) continue;
|
|
1241
1869
|
try {
|
|
1242
1870
|
const remoteEvents = normalizePendingMeshCoordinatorEvents(
|
|
1243
|
-
await ctx.transport.meshCommand(node.daemonId, "get_pending_mesh_events",
|
|
1871
|
+
await ctx.transport.meshCommand(node.daemonId, "get_pending_mesh_events", pendingEventArgs)
|
|
1244
1872
|
).filter(matchesCurrentMesh);
|
|
1245
1873
|
if (remoteEvents.length === 0) continue;
|
|
1246
1874
|
for (const event of remoteEvents) {
|
|
1247
1875
|
const payload = buildMeshForwardPayloadFromPendingEvent(event);
|
|
1248
1876
|
if (!payload.event || !payload.meshId) continue;
|
|
1249
1877
|
await ctx.transport.command("mesh_forward_event", payload);
|
|
1878
|
+
rememberMeshSessionProviderMetadataFromEvent({ ...event, metadataEvent: payload });
|
|
1250
1879
|
}
|
|
1251
1880
|
} catch {
|
|
1252
1881
|
}
|
|
1253
1882
|
}
|
|
1254
1883
|
try {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1884
|
+
const localEvents = normalizePendingMeshCoordinatorEvents(await ctx.transport.command("get_pending_mesh_events", pendingEventArgs)).filter(matchesCurrentMesh);
|
|
1885
|
+
for (const event of localEvents) {
|
|
1886
|
+
const payload = buildMeshForwardPayloadFromPendingEvent(event);
|
|
1887
|
+
if (!payload.event || !payload.meshId) continue;
|
|
1888
|
+
let injected = false;
|
|
1889
|
+
try {
|
|
1890
|
+
await ctx.transport.command("mesh_forward_event", payload);
|
|
1891
|
+
injected = true;
|
|
1892
|
+
} catch {
|
|
1893
|
+
}
|
|
1894
|
+
rememberMeshSessionProviderMetadataFromEvent({ ...event, metadataEvent: payload });
|
|
1895
|
+
if (!injected) surfacedEvents.push(event);
|
|
1896
|
+
}
|
|
1258
1897
|
} catch {
|
|
1259
1898
|
}
|
|
1260
1899
|
return surfacedEvents;
|
|
1261
1900
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
return [];
|
|
1901
|
+
const events = (0, import_daemon_core.drainPendingMeshCoordinatorEvents)(ctx.mesh.id, ctx.localDaemonId).filter(matchesCurrentMesh);
|
|
1902
|
+
events.forEach(rememberMeshSessionProviderMetadataFromEvent);
|
|
1903
|
+
return events;
|
|
1266
1904
|
}
|
|
1267
1905
|
function isP2pTransportUnavailableError(error) {
|
|
1268
1906
|
return (0, import_daemon_core.isP2pRelayTransportFailure)(error);
|
|
@@ -1277,11 +1915,12 @@ function buildRemoveNodeArgs(ctx, nodeId, sessionCleanupMode) {
|
|
|
1277
1915
|
}
|
|
1278
1916
|
var MESH_STATUS_TOOL = {
|
|
1279
1917
|
name: "mesh_status",
|
|
1280
|
-
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.",
|
|
1918
|
+
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.",
|
|
1281
1919
|
inputSchema: {
|
|
1282
1920
|
type: "object",
|
|
1283
1921
|
properties: {
|
|
1284
|
-
_gemini_compat: { type: "string", description: "Dummy property for Gemini compatibility. Ignore this." }
|
|
1922
|
+
_gemini_compat: { type: "string", description: "Dummy property for Gemini compatibility. Ignore this." },
|
|
1923
|
+
includeStaleDirectWorkDetails: { type: "boolean", description: "Opt in to the full staleDirectWork array. Defaults false; normal status returns compact staleDirectWorkSummary only." }
|
|
1285
1924
|
}
|
|
1286
1925
|
}
|
|
1287
1926
|
};
|
|
@@ -1301,14 +1940,22 @@ var MESH_ENQUEUE_TASK_TOOL = {
|
|
|
1301
1940
|
inputSchema: {
|
|
1302
1941
|
type: "object",
|
|
1303
1942
|
properties: {
|
|
1304
|
-
message: { type: "string", description: "The task instruction for the agent." }
|
|
1943
|
+
message: { type: "string", description: "The task instruction for the agent." },
|
|
1944
|
+
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." },
|
|
1945
|
+
taskMode: { type: "string", enum: ["code_change", "validation", "live_debug_readonly", "launch_app", "convergence"], description: "CamelCase alias for task_mode." },
|
|
1946
|
+
requiredTags: { type: "array", items: { type: "string" }, description: "Optional capability tags that every eligible node must have, e.g. os=darwin, provider=codex-cli, gpu." },
|
|
1947
|
+
required_tags: { type: "array", items: { type: "string" }, description: "Snake_case alias for requiredTags." },
|
|
1948
|
+
depends_on: { type: "array", items: { type: "string" }, description: "Task ids that must complete before this task becomes claimable. Cycles are rejected at enqueue." },
|
|
1949
|
+
dependsOn: { type: "array", items: { type: "string" }, description: "CamelCase alias for depends_on." },
|
|
1950
|
+
mission_id: { type: "string", description: "Mission this task belongs to (mesh_mission record id)." },
|
|
1951
|
+
missionId: { type: "string", description: "CamelCase alias for mission_id." }
|
|
1305
1952
|
},
|
|
1306
1953
|
required: ["message"]
|
|
1307
1954
|
}
|
|
1308
1955
|
};
|
|
1309
1956
|
var MESH_VIEW_QUEUE_TOOL = {
|
|
1310
1957
|
name: "mesh_view_queue",
|
|
1311
|
-
description: "View the mesh work queue with source-of-truth active counts separated from historical completed/failed/cancelled records.",
|
|
1958
|
+
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.",
|
|
1312
1959
|
inputSchema: {
|
|
1313
1960
|
type: "object",
|
|
1314
1961
|
properties: {
|
|
@@ -1339,7 +1986,7 @@ var MESH_QUEUE_CANCEL_TOOL = {
|
|
|
1339
1986
|
};
|
|
1340
1987
|
var MESH_QUEUE_REQUEUE_TOOL = {
|
|
1341
1988
|
name: "mesh_queue_requeue",
|
|
1342
|
-
description: "Return a mesh queue task to pending for retry. By default clears stale assigned owner and target session so another live session can claim it.",
|
|
1989
|
+
description: "Return a mesh queue task to pending for retry. By default clears stale assigned owner and target session so another live session can claim it. When the task has exceeded its retry cap it is auto-failed instead; use force=true to override.",
|
|
1343
1990
|
inputSchema: {
|
|
1344
1991
|
type: "object",
|
|
1345
1992
|
properties: {
|
|
@@ -1348,7 +1995,8 @@ var MESH_QUEUE_REQUEUE_TOOL = {
|
|
|
1348
1995
|
target_node_id: { type: "string", description: "Optional replacement target node ID." },
|
|
1349
1996
|
target_session_id: { type: "string", description: "Optional replacement target runtime session ID." },
|
|
1350
1997
|
clear_target_node: { type: "boolean", description: "When true, remove any existing target node constraint." },
|
|
1351
|
-
keep_target_session: { type: "boolean", description: "When true, preserve an existing target session if target_session_id is not provided. Defaults false to avoid stale session targets." }
|
|
1998
|
+
keep_target_session: { type: "boolean", description: "When true, preserve an existing target session if target_session_id is not provided. Defaults false to avoid stale session targets." },
|
|
1999
|
+
force: { type: "boolean", description: "When true, bypass the retry cap and requeue even if maxRetries has been exceeded. Use only for explicit operator recovery." }
|
|
1352
2000
|
},
|
|
1353
2001
|
required: ["task_id"]
|
|
1354
2002
|
}
|
|
@@ -1361,7 +2009,9 @@ var MESH_SEND_TASK_TOOL = {
|
|
|
1361
2009
|
properties: {
|
|
1362
2010
|
node_id: { type: "string", description: "Target node ID (from mesh_list_nodes)." },
|
|
1363
2011
|
session_id: { type: "string", description: "Agent session ID on the target node." },
|
|
1364
|
-
message: { type: "string", description: "Natural-language task to send to the agent." }
|
|
2012
|
+
message: { type: "string", description: "Natural-language task to send to the agent." },
|
|
2013
|
+
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." },
|
|
2014
|
+
taskMode: { type: "string", enum: ["code_change", "validation", "live_debug_readonly", "launch_app", "convergence"], description: "CamelCase alias for task_mode." }
|
|
1365
2015
|
},
|
|
1366
2016
|
required: ["node_id", "session_id", "message"]
|
|
1367
2017
|
}
|
|
@@ -1419,6 +2069,21 @@ var MESH_GIT_STATUS_TOOL = {
|
|
|
1419
2069
|
required: ["node_id"]
|
|
1420
2070
|
}
|
|
1421
2071
|
};
|
|
2072
|
+
var MESH_FAST_FORWARD_NODE_TOOL = {
|
|
2073
|
+
name: "mesh_fast_forward_node",
|
|
2074
|
+
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.",
|
|
2075
|
+
inputSchema: {
|
|
2076
|
+
type: "object",
|
|
2077
|
+
properties: {
|
|
2078
|
+
node_id: { type: "string", description: "Target node ID." },
|
|
2079
|
+
branch: { type: "string", description: "Optional guard: require the node's current branch to match this branch before planning/executing." },
|
|
2080
|
+
execute: { type: "boolean", description: "When true, apply the fast-forward if all safety gates pass. Defaults false/dry-run." },
|
|
2081
|
+
dry_run: { type: "boolean", description: "Preview only. Defaults true unless execute=true; dry_run=true overrides execute." },
|
|
2082
|
+
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." }
|
|
2083
|
+
},
|
|
2084
|
+
required: ["node_id"]
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
1422
2087
|
var MESH_CHECKPOINT_TOOL = {
|
|
1423
2088
|
name: "mesh_checkpoint",
|
|
1424
2089
|
description: "Create a git checkpoint (commit) on a mesh node workspace.",
|
|
@@ -1431,13 +2096,27 @@ var MESH_CHECKPOINT_TOOL = {
|
|
|
1431
2096
|
required: ["node_id", "message"]
|
|
1432
2097
|
}
|
|
1433
2098
|
};
|
|
1434
|
-
var
|
|
1435
|
-
name: "
|
|
1436
|
-
description: "
|
|
2099
|
+
var MESH_MISSION_UPSERT_TOOL = {
|
|
2100
|
+
name: "mesh_mission_upsert",
|
|
2101
|
+
description: "Create or update a persistent mission record so the plan survives coordinator restarts. Create a mission before enqueueing a multi-task batch, attach tasks via mesh_enqueue_task mission_id, and update status to completed/abandoned when the outcome is decided. Progress is derived from task statuses \u2014 there is no separate progress field.",
|
|
1437
2102
|
inputSchema: {
|
|
1438
2103
|
type: "object",
|
|
1439
2104
|
properties: {
|
|
1440
|
-
|
|
2105
|
+
mission_id: { type: "string", description: "Mission id to update. Omit to create a new mission." },
|
|
2106
|
+
title: { type: "string", description: "Short mission title." },
|
|
2107
|
+
goal: { type: "string", description: "Free-text mission goal/definition of done." },
|
|
2108
|
+
status: { type: "string", enum: ["active", "paused", "completed", "abandoned"], description: "Mission lifecycle status. Defaults to active on create." }
|
|
2109
|
+
},
|
|
2110
|
+
required: ["title"]
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
var MESH_APPROVE_TOOL = {
|
|
2114
|
+
name: "mesh_approve",
|
|
2115
|
+
description: "Approve or reject a pending action on a delegated agent session.",
|
|
2116
|
+
inputSchema: {
|
|
2117
|
+
type: "object",
|
|
2118
|
+
properties: {
|
|
2119
|
+
node_id: { type: "string", description: "Target node ID." },
|
|
1441
2120
|
session_id: { type: "string", description: "Agent session ID with pending approval." },
|
|
1442
2121
|
action: { type: "string", enum: ["approve", "reject"], description: "Action to take." }
|
|
1443
2122
|
},
|
|
@@ -1502,7 +2181,7 @@ var MESH_TASK_HISTORY_TOOL = {
|
|
|
1502
2181
|
type: "object",
|
|
1503
2182
|
properties: {
|
|
1504
2183
|
tail: { type: "number", description: "Number of recent entries to return (default: 20)." },
|
|
1505
|
-
kind: { type: "string", description: "Filter by entry kind: task_dispatched, task_completed, task_failed, task_stalled, session_launched, checkpoint_created, node_cloned, node_removed." }
|
|
2184
|
+
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." }
|
|
1506
2185
|
}
|
|
1507
2186
|
}
|
|
1508
2187
|
};
|
|
@@ -1522,7 +2201,7 @@ var MESH_RECONCILE_LEDGER_TOOL = {
|
|
|
1522
2201
|
};
|
|
1523
2202
|
var MESH_REFINE_NODE_TOOL = {
|
|
1524
2203
|
name: "mesh_refine_node",
|
|
1525
|
-
description: "The Refinery:
|
|
2204
|
+
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.",
|
|
1526
2205
|
inputSchema: {
|
|
1527
2206
|
type: "object",
|
|
1528
2207
|
properties: {
|
|
@@ -1531,6 +2210,54 @@ var MESH_REFINE_NODE_TOOL = {
|
|
|
1531
2210
|
required: ["node_id"]
|
|
1532
2211
|
}
|
|
1533
2212
|
};
|
|
2213
|
+
var MESH_REFINE_CONFIG_SCHEMA_TOOL = {
|
|
2214
|
+
name: "mesh_refine_config_schema",
|
|
2215
|
+
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.",
|
|
2216
|
+
inputSchema: { type: "object", properties: {} }
|
|
2217
|
+
};
|
|
2218
|
+
var MESH_VALIDATE_REFINE_CONFIG_TOOL = {
|
|
2219
|
+
name: "mesh_validate_refine_config",
|
|
2220
|
+
description: "Validate the repo mesh/refine config for a node/workspace without running validation commands or merging.",
|
|
2221
|
+
inputSchema: {
|
|
2222
|
+
type: "object",
|
|
2223
|
+
properties: {
|
|
2224
|
+
node_id: { type: "string", description: "Optional node/workspace whose refine config should be loaded. Defaults to the first mesh node." },
|
|
2225
|
+
config: { type: "object", description: "Optional inline config object to validate instead of loading from the repo." }
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
var MESH_SUGGEST_REFINE_CONFIG_TOOL = {
|
|
2230
|
+
name: "mesh_suggest_refine_config",
|
|
2231
|
+
description: "Suggest a repo mesh/refine config scaffold from project context/package scripts. Suggestions are never executed until saved as explicit refine config.",
|
|
2232
|
+
inputSchema: {
|
|
2233
|
+
type: "object",
|
|
2234
|
+
properties: {
|
|
2235
|
+
node_id: { type: "string", description: "Optional node/workspace used for suggestions. Defaults to the first mesh node." }
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
var MESH_REFINE_PLAN_TOOL = {
|
|
2240
|
+
name: "mesh_refine_plan",
|
|
2241
|
+
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.",
|
|
2242
|
+
inputSchema: {
|
|
2243
|
+
type: "object",
|
|
2244
|
+
properties: {
|
|
2245
|
+
node_id: { type: "string", description: "Node ID of the worktree node to plan." }
|
|
2246
|
+
},
|
|
2247
|
+
required: ["node_id"]
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
var MESH_REVIEW_INBOX_TOOL = {
|
|
2251
|
+
name: "mesh_review_inbox",
|
|
2252
|
+
description: "List local worktree nodes that need human review: merge candidates (pushed feature branches ready to merge) and Refinery-blocked review results. Returns evidence summaries, diff stats vs. the default branch, and suggested actions (Refine / Requeue / Dismiss). Remote nodes are excluded in M4.0.",
|
|
2253
|
+
inputSchema: {
|
|
2254
|
+
type: "object",
|
|
2255
|
+
properties: {
|
|
2256
|
+
mesh_id: { type: "string", description: "Mesh ID (optional \u2014 inferred from active mesh if omitted)." }
|
|
2257
|
+
},
|
|
2258
|
+
required: []
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
1534
2261
|
var ALL_MESH_TOOLS = [
|
|
1535
2262
|
MESH_STATUS_TOOL,
|
|
1536
2263
|
MESH_LIST_NODES_TOOL,
|
|
@@ -1543,66 +2270,57 @@ var ALL_MESH_TOOLS = [
|
|
|
1543
2270
|
MESH_READ_DEBUG_TOOL,
|
|
1544
2271
|
MESH_LAUNCH_SESSION_TOOL,
|
|
1545
2272
|
MESH_GIT_STATUS_TOOL,
|
|
2273
|
+
MESH_FAST_FORWARD_NODE_TOOL,
|
|
1546
2274
|
MESH_CHECKPOINT_TOOL,
|
|
1547
2275
|
MESH_APPROVE_TOOL,
|
|
1548
2276
|
MESH_CLONE_NODE_TOOL,
|
|
1549
2277
|
MESH_REMOVE_NODE_TOOL,
|
|
1550
2278
|
MESH_REFINE_NODE_TOOL,
|
|
2279
|
+
MESH_REFINE_CONFIG_SCHEMA_TOOL,
|
|
2280
|
+
MESH_VALIDATE_REFINE_CONFIG_TOOL,
|
|
2281
|
+
MESH_SUGGEST_REFINE_CONFIG_TOOL,
|
|
2282
|
+
MESH_REFINE_PLAN_TOOL,
|
|
1551
2283
|
MESH_CLEANUP_SESSIONS_TOOL,
|
|
1552
2284
|
MESH_TASK_HISTORY_TOOL,
|
|
1553
|
-
MESH_RECONCILE_LEDGER_TOOL
|
|
2285
|
+
MESH_RECONCILE_LEDGER_TOOL,
|
|
2286
|
+
MESH_MISSION_UPSERT_TOOL,
|
|
2287
|
+
MESH_REVIEW_INBOX_TOOL
|
|
1554
2288
|
];
|
|
1555
|
-
async function meshStatus(ctx) {
|
|
2289
|
+
async function meshStatus(ctx, args = {}) {
|
|
2290
|
+
const rateResult = (0, import_daemon_core.recordMeshToolCall)({ meshId: ctx.mesh.id, tool: "mesh_status" });
|
|
1556
2291
|
await refreshMeshFromDaemon(ctx);
|
|
1557
2292
|
const { mesh, transport } = ctx;
|
|
1558
|
-
|
|
1559
|
-
const
|
|
1560
|
-
for (const node of mesh.nodes) {
|
|
2293
|
+
let ledgerSummary = (0, import_daemon_core.getLedgerSummary)(mesh.id);
|
|
2294
|
+
const results = await Promise.all(mesh.nodes.map(async (node) => {
|
|
1561
2295
|
const entry = {
|
|
1562
2296
|
nodeId: node.id,
|
|
1563
2297
|
workspace: node.workspace,
|
|
2298
|
+
machine: buildNodeMachineIdentity(ctx, node),
|
|
2299
|
+
daemonId: readNodeDaemonId(node),
|
|
2300
|
+
machineId: readNodeMachineId(node),
|
|
1564
2301
|
...getNodeLaunchReadiness(node)
|
|
1565
2302
|
};
|
|
1566
2303
|
try {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
includeSubmodules: autoDiscover,
|
|
1588
|
-
submoduleIgnorePaths: node.policy?.submoduleIgnorePaths || void 0
|
|
1589
|
-
});
|
|
1590
|
-
const status = extractGitStatus(statusResult);
|
|
1591
|
-
const uncommittedChanges = countUncommittedChanges(status);
|
|
1592
|
-
const dirty = isGitStatusDirty(status);
|
|
1593
|
-
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
1594
|
-
entry.branch = status?.branch;
|
|
1595
|
-
entry.isDirty = dirty;
|
|
1596
|
-
entry.uncommittedChanges = uncommittedChanges;
|
|
1597
|
-
entry.branchConvergence = buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges);
|
|
1598
|
-
const submodules = extractSubmodules(statusResult, node.policy?.submoduleIgnorePaths || []);
|
|
1599
|
-
if (submodules && submodules.some((s) => s?.outOfSync)) {
|
|
1600
|
-
entry.submoduleWarning = "One or more submodules are out of sync with the parent repo. Run `git submodule update` or check deployment readiness.";
|
|
1601
|
-
entry.outOfSyncSubmodules = submodules.filter((s) => s?.outOfSync).map((s) => s.path);
|
|
1602
|
-
}
|
|
1603
|
-
} else {
|
|
1604
|
-
entry.health = "unknown";
|
|
1605
|
-
entry.note = "No daemonId available for cloud status probe";
|
|
2304
|
+
const autoDiscover = node.policy?.autoDiscoverSubmodules !== false;
|
|
2305
|
+
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
2306
|
+
workspace: node.workspace,
|
|
2307
|
+
refreshUpstream: true,
|
|
2308
|
+
includeSubmodules: autoDiscover,
|
|
2309
|
+
submoduleIgnorePaths: node.policy?.submoduleIgnorePaths || void 0
|
|
2310
|
+
});
|
|
2311
|
+
const status = extractGitStatus(statusResult);
|
|
2312
|
+
const uncommittedChanges = countUncommittedChanges(status);
|
|
2313
|
+
const dirty = isGitStatusDirty(status);
|
|
2314
|
+
entry.health = status?.isGitRepo ? dirty ? "dirty" : "online" : "degraded";
|
|
2315
|
+
assignFullGitSnapshot(entry, status);
|
|
2316
|
+
entry.branch = status?.branch;
|
|
2317
|
+
entry.isDirty = dirty;
|
|
2318
|
+
entry.uncommittedChanges = uncommittedChanges;
|
|
2319
|
+
entry.branchConvergence = buildBranchConvergence(mesh, node, status, dirty, uncommittedChanges);
|
|
2320
|
+
const submodules = extractSubmodules(statusResult, node.policy?.submoduleIgnorePaths || []);
|
|
2321
|
+
if (submodules && submodules.some((s) => s?.outOfSync)) {
|
|
2322
|
+
entry.submoduleWarning = "One or more submodules are out of sync with the parent repo. Run `git submodule update` or check deployment readiness.";
|
|
2323
|
+
entry.outOfSyncSubmodules = submodules.filter((s) => s?.outOfSync).map((s) => s.path);
|
|
1606
2324
|
}
|
|
1607
2325
|
} catch (e) {
|
|
1608
2326
|
const failure = buildCoordinatorP2pRelayFailure(e, {
|
|
@@ -1626,7 +2344,7 @@ async function meshStatus(ctx) {
|
|
|
1626
2344
|
if (recoveryContext.consecutiveNodeFailures > 0) {
|
|
1627
2345
|
entry.recoveryHints = {
|
|
1628
2346
|
consecutiveFailures: recoveryContext.consecutiveNodeFailures,
|
|
1629
|
-
lastTaskMessage: recoveryContext.lastTaskMessage,
|
|
2347
|
+
lastTaskMessage: typeof recoveryContext.lastTaskMessage === "string" ? recoveryContext.lastTaskMessage.slice(0, 100) + (recoveryContext.lastTaskMessage.length > 100 ? "\u2026" : "") : recoveryContext.lastTaskMessage,
|
|
1630
2348
|
advice: recoveryContext.advice,
|
|
1631
2349
|
retryRecommended: recoveryContext.retryRecommended
|
|
1632
2350
|
};
|
|
@@ -1666,7 +2384,55 @@ async function meshStatus(ctx) {
|
|
|
1666
2384
|
}
|
|
1667
2385
|
const relatedRepos = await collectRelatedRepoStatuses(ctx, node);
|
|
1668
2386
|
if (relatedRepos.length) entry.relatedRepos = relatedRepos;
|
|
1669
|
-
|
|
2387
|
+
const liveSessions = await collectLiveStatusSessions(ctx, node);
|
|
2388
|
+
if (liveSessions.length > 0) {
|
|
2389
|
+
entry.sessions = liveSessions.map((s) => {
|
|
2390
|
+
const coordinatorMeshId = typeof s.coordinator?.meshId === "string" ? s.coordinator.meshId : void 0;
|
|
2391
|
+
const isSelfCoordinator = coordinatorMeshId === mesh.id;
|
|
2392
|
+
return {
|
|
2393
|
+
id: s.instanceId ?? s.id ?? s.sessionId,
|
|
2394
|
+
status: s.status ?? s.lifecycle ?? s.state,
|
|
2395
|
+
providerType: s.providerType ?? s.cliType ?? s.type,
|
|
2396
|
+
...s.activeChat?.status ? { chatStatus: s.activeChat.status } : {},
|
|
2397
|
+
...isSelfCoordinator ? { isSelfCoordinator: true, role: "coordinator" } : {}
|
|
2398
|
+
};
|
|
2399
|
+
}).filter((s) => s.id);
|
|
2400
|
+
}
|
|
2401
|
+
return entry;
|
|
2402
|
+
}));
|
|
2403
|
+
let ledgerEntries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail: 200 });
|
|
2404
|
+
let directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(mesh.id);
|
|
2405
|
+
const directReconciliation = await reconcileDirectDispatchesFromTranscriptEvidence(ctx, results, directDispatches, ledgerEntries);
|
|
2406
|
+
if (directReconciliation.reconciled > 0) {
|
|
2407
|
+
ledgerEntries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail: 200 });
|
|
2408
|
+
directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(mesh.id);
|
|
2409
|
+
ledgerSummary = (0, import_daemon_core.getLedgerSummary)(mesh.id);
|
|
2410
|
+
}
|
|
2411
|
+
const activeWorkEvidence = (0, import_daemon_core.buildMeshActiveWork)({
|
|
2412
|
+
meshId: mesh.id,
|
|
2413
|
+
queue: (0, import_daemon_core.getQueue)(mesh.id),
|
|
2414
|
+
ledgerEntries,
|
|
2415
|
+
directDispatches,
|
|
2416
|
+
nodes: results
|
|
2417
|
+
});
|
|
2418
|
+
const pollingGuidance = buildActiveWorkPollingGuidance(activeWorkEvidence.summary);
|
|
2419
|
+
const staleDirectWorkSummary = (0, import_daemon_core.buildCompactStaleDirectWorkSummary)(activeWorkEvidence.staleDirectWork, {
|
|
2420
|
+
note: activeWorkEvidence.staleDirectWorkNote,
|
|
2421
|
+
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."
|
|
2422
|
+
});
|
|
2423
|
+
const coordinatorSessions = [];
|
|
2424
|
+
for (const nodeEntry of results) {
|
|
2425
|
+
const sessions = Array.isArray(nodeEntry.sessions) ? nodeEntry.sessions : [];
|
|
2426
|
+
for (const s of sessions) {
|
|
2427
|
+
if (s?.isSelfCoordinator === true && s.id) {
|
|
2428
|
+
coordinatorSessions.push({
|
|
2429
|
+
nodeId: nodeEntry.nodeId,
|
|
2430
|
+
sessionId: s.id,
|
|
2431
|
+
providerType: s.providerType,
|
|
2432
|
+
status: s.status
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
1670
2436
|
}
|
|
1671
2437
|
const response = {
|
|
1672
2438
|
meshId: mesh.id,
|
|
@@ -1677,17 +2443,55 @@ async function meshStatus(ctx) {
|
|
|
1677
2443
|
sourceOfTruth: {
|
|
1678
2444
|
membership: "coordinator_daemon_live_mesh",
|
|
1679
2445
|
currentStatus: "live_git_and_session_probes",
|
|
2446
|
+
activeWork: "mesh_queue_file_and_local_ledger",
|
|
1680
2447
|
historicalEvidenceOnly: ["recoveryHints", "ledgerSummary"]
|
|
1681
2448
|
},
|
|
1682
2449
|
nodes: results,
|
|
1683
|
-
|
|
2450
|
+
activeWork: activeWorkEvidence.activeWork,
|
|
2451
|
+
staleDirectWorkSummary,
|
|
2452
|
+
...args.includeStaleDirectWorkDetails === true ? { staleDirectWork: activeWorkEvidence.staleDirectWork } : {},
|
|
2453
|
+
// terminalDirectWork is historical (completed/failed direct dispatches) — opt-in only.
|
|
2454
|
+
...args.includeTerminalDirectWork === true ? { terminalDirectWork: activeWorkEvidence.terminalDirectWork } : {},
|
|
2455
|
+
activeWorkSummary: activeWorkEvidence.summary,
|
|
2456
|
+
...pollingGuidance ? { pollingGuidance } : {},
|
|
2457
|
+
...rateResult.rateLimitExceeded ? { pollingRateAdvisory: { type: "rate_limit_exceeded", tool: "mesh_status", callsInWindow: rateResult.callsInWindow, message: rateResult.advisory } } : {},
|
|
2458
|
+
branchConvergenceSummary: summarizeBranchConvergence(results),
|
|
2459
|
+
...coordinatorSessions.length > 0 ? {
|
|
2460
|
+
coordinatorSessions,
|
|
2461
|
+
selfIdentification: {
|
|
2462
|
+
meshId: mesh.id,
|
|
2463
|
+
coordinatorSessions,
|
|
2464
|
+
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."
|
|
2465
|
+
}
|
|
2466
|
+
} : {}
|
|
1684
2467
|
};
|
|
1685
2468
|
try {
|
|
1686
2469
|
response.ledgerSummary = ledgerSummary;
|
|
1687
2470
|
} catch {
|
|
1688
2471
|
}
|
|
2472
|
+
try {
|
|
2473
|
+
const missions = (0, import_daemon_core.getActiveMeshMissionSummaries)(mesh.id);
|
|
2474
|
+
if (missions.length > 0) {
|
|
2475
|
+
response.missions = missions.map((mission) => {
|
|
2476
|
+
try {
|
|
2477
|
+
return { ...mission, stats: (0, import_daemon_core.computeMeshMissionStats)(mesh.id, mission.id) };
|
|
2478
|
+
} catch {
|
|
2479
|
+
return mission;
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
} catch {
|
|
2484
|
+
}
|
|
1689
2485
|
try {
|
|
1690
2486
|
const pendingEvents = await drainCoordinatorPendingEvents(ctx);
|
|
2487
|
+
const asyncRefineJobs = (0, import_daemon_core.buildMeshAsyncRefineJobs)({
|
|
2488
|
+
meshId: mesh.id,
|
|
2489
|
+
ledgerEntries,
|
|
2490
|
+
pendingEvents
|
|
2491
|
+
});
|
|
2492
|
+
if (asyncRefineJobs.length > 0) {
|
|
2493
|
+
response.asyncRefineJobs = asyncRefineJobs;
|
|
2494
|
+
}
|
|
1691
2495
|
if (pendingEvents.length > 0) {
|
|
1692
2496
|
response.pendingCoordinatorEvents = pendingEvents;
|
|
1693
2497
|
}
|
|
@@ -1697,12 +2501,31 @@ async function meshStatus(ctx) {
|
|
|
1697
2501
|
}
|
|
1698
2502
|
async function meshTaskHistory(ctx, args) {
|
|
1699
2503
|
const { mesh } = ctx;
|
|
1700
|
-
await drainCoordinatorPendingEvents(ctx);
|
|
2504
|
+
const pendingEvents = await drainCoordinatorPendingEvents(ctx);
|
|
1701
2505
|
const tail = typeof args.tail === "number" && args.tail > 0 ? args.tail : 20;
|
|
1702
2506
|
const kind = typeof args.kind === "string" && args.kind.trim() ? [args.kind.trim()] : void 0;
|
|
1703
|
-
const
|
|
2507
|
+
const rawEntries = (0, import_daemon_core.readLedgerEntries)(mesh.id, { tail, kind });
|
|
2508
|
+
const entries = rawEntries.map((e) => ({
|
|
2509
|
+
...e,
|
|
2510
|
+
payload: e.payload ? slimLedgerPayload(e.payload) : e.payload
|
|
2511
|
+
}));
|
|
1704
2512
|
const summary = (0, import_daemon_core.getLedgerSummary)(mesh.id);
|
|
1705
|
-
|
|
2513
|
+
let taskStats;
|
|
2514
|
+
try {
|
|
2515
|
+
const taskIds = [...new Set(rawEntries.map((e) => typeof e.payload?.taskId === "string" ? e.payload.taskId : "").filter(Boolean))];
|
|
2516
|
+
if (taskIds.length > 0) {
|
|
2517
|
+
const stats = (0, import_daemon_core.computeMeshTaskStats)(mesh.id, { taskIds });
|
|
2518
|
+
if (stats.length > 0) taskStats = stats;
|
|
2519
|
+
}
|
|
2520
|
+
} catch {
|
|
2521
|
+
}
|
|
2522
|
+
return JSON.stringify({
|
|
2523
|
+
meshId: mesh.id,
|
|
2524
|
+
entries,
|
|
2525
|
+
summary,
|
|
2526
|
+
...taskStats ? { taskStats } : {},
|
|
2527
|
+
...pendingEvents.length > 0 ? { pendingCoordinatorEvents: pendingEvents } : {}
|
|
2528
|
+
}, null, 2);
|
|
1706
2529
|
}
|
|
1707
2530
|
async function meshReconcileLedger(ctx, args) {
|
|
1708
2531
|
await refreshMeshFromDaemon(ctx);
|
|
@@ -1719,7 +2542,7 @@ async function meshReconcileLedger(ctx, args) {
|
|
|
1719
2542
|
for (const node of nodes) {
|
|
1720
2543
|
try {
|
|
1721
2544
|
if (isLocalControlPlaneNode(ctx, node) || !node.daemonId) {
|
|
1722
|
-
const slice2 = (0, import_daemon_core.
|
|
2545
|
+
const slice2 = (0, import_daemon_core.readLedgerSliceFromStore)(ctx.mesh.id, queryArgs);
|
|
1723
2546
|
replicas.push((0, import_daemon_core.buildMeshLedgerReplicaEvidence)({
|
|
1724
2547
|
nodeId: node.id,
|
|
1725
2548
|
daemonId: node.daemonId,
|
|
@@ -1792,6 +2615,9 @@ async function meshListNodes(ctx) {
|
|
|
1792
2615
|
nodeId: n.id,
|
|
1793
2616
|
workspace: n.workspace,
|
|
1794
2617
|
repoRoot: n.repoRoot,
|
|
2618
|
+
daemonId: readNodeDaemonId(n),
|
|
2619
|
+
machineId: readNodeMachineId(n),
|
|
2620
|
+
machine: buildNodeMachineIdentity(ctx, n),
|
|
1795
2621
|
isLocalWorktree: n.isLocalWorktree,
|
|
1796
2622
|
policy: n.policy,
|
|
1797
2623
|
relatedRepos: readRelatedRepos(n),
|
|
@@ -1800,58 +2626,167 @@ async function meshListNodes(ctx) {
|
|
|
1800
2626
|
}))
|
|
1801
2627
|
}, null, 2);
|
|
1802
2628
|
}
|
|
2629
|
+
async function meshMissionUpsert(ctx, args) {
|
|
2630
|
+
try {
|
|
2631
|
+
const mission = (0, import_daemon_core.upsertMeshMission)(ctx.mesh.id, {
|
|
2632
|
+
id: readString(args.mission_id) || readString(args.missionId) || void 0,
|
|
2633
|
+
title: args.title,
|
|
2634
|
+
goal: typeof args.goal === "string" ? args.goal : void 0,
|
|
2635
|
+
status: readString(args.status) || void 0
|
|
2636
|
+
});
|
|
2637
|
+
return JSON.stringify({
|
|
2638
|
+
success: true,
|
|
2639
|
+
mission,
|
|
2640
|
+
nextAction: "Attach tasks with mesh_enqueue_task mission_id and depends_on. mesh_status shows live task aggregates for this mission."
|
|
2641
|
+
});
|
|
2642
|
+
} catch (e) {
|
|
2643
|
+
const message = e?.message || String(e);
|
|
2644
|
+
const code = message.includes("mission_title_required") ? "mission_title_required" : message.includes("invalid_mission_status") ? "invalid_mission_status" : void 0;
|
|
2645
|
+
return JSON.stringify({ success: false, ...code ? { code } : {}, error: message });
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
1803
2648
|
async function meshEnqueueTask(ctx, args) {
|
|
2649
|
+
const taskMode = readString(args.task_mode) || readString(args.taskMode);
|
|
2650
|
+
const requiredTags = (0, import_daemon_core.normalizeMeshCapabilityTags)(Array.isArray(args.requiredTags) ? args.requiredTags : args.required_tags);
|
|
2651
|
+
const dependsOn = Array.isArray(args.dependsOn) ? args.dependsOn : Array.isArray(args.depends_on) ? args.depends_on : void 0;
|
|
2652
|
+
const missionId = readString(args.missionId) || readString(args.mission_id) || void 0;
|
|
1804
2653
|
try {
|
|
1805
|
-
const task = (0, import_daemon_core.enqueueTask)(ctx.mesh.id, args.message);
|
|
1806
|
-
if (
|
|
1807
|
-
|
|
2654
|
+
const task = (0, import_daemon_core.enqueueTask)(ctx.mesh.id, args.message, { taskMode, requiredTags, dependsOn, missionId });
|
|
2655
|
+
if (!(ctx.transport instanceof IpcTransport)) {
|
|
2656
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx);
|
|
2657
|
+
return JSON.stringify({
|
|
2658
|
+
success: true,
|
|
2659
|
+
source: "queue",
|
|
2660
|
+
taskId: task.id,
|
|
2661
|
+
status: task.status,
|
|
2662
|
+
taskMode: task.taskMode,
|
|
2663
|
+
requiredTags: task.requiredTags,
|
|
2664
|
+
queueTrigger,
|
|
2665
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
1808
2666
|
});
|
|
1809
|
-
return JSON.stringify({ success: true, taskId: task.id, status: task.status });
|
|
1810
2667
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
});
|
|
2668
|
+
{
|
|
2669
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx);
|
|
1814
2670
|
const dispatchPromises = [];
|
|
1815
2671
|
for (const node of ctx.mesh.nodes) {
|
|
1816
2672
|
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
1817
2673
|
if (isLocalNode || !node.daemonId) continue;
|
|
2674
|
+
if (!(0, import_daemon_core.nodeSatisfiesRequiredTags)(requiredTags, (0, import_daemon_core.buildMeshNodeCapabilityTags)(node))) continue;
|
|
1818
2675
|
dispatchPromises.push(
|
|
1819
2676
|
ipcDispatchToRemoteAgent(ctx, node, { message: args.message }).then((result) => {
|
|
1820
2677
|
if (result.success) {
|
|
1821
2678
|
try {
|
|
2679
|
+
const providerType = result.providerType;
|
|
2680
|
+
const descriptor = summarizeTaskMessage(args.message);
|
|
1822
2681
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
1823
2682
|
kind: "task_dispatched",
|
|
1824
2683
|
nodeId: node.id,
|
|
1825
2684
|
sessionId: result.sessionId,
|
|
1826
|
-
|
|
2685
|
+
providerType,
|
|
2686
|
+
payload: {
|
|
2687
|
+
source: "queue",
|
|
2688
|
+
via: "p2p_direct",
|
|
2689
|
+
taskId: task.id,
|
|
2690
|
+
message: args.message,
|
|
2691
|
+
taskTitle: descriptor.taskTitle,
|
|
2692
|
+
taskSummary: descriptor.taskSummary,
|
|
2693
|
+
...task.taskMode ? { taskMode: task.taskMode } : {},
|
|
2694
|
+
...providerType ? { providerType } : {},
|
|
2695
|
+
targetSessionId: result.sessionId
|
|
2696
|
+
}
|
|
1827
2697
|
});
|
|
1828
2698
|
} catch {
|
|
1829
2699
|
}
|
|
1830
2700
|
}
|
|
1831
|
-
}).catch(() => {
|
|
2701
|
+
}).catch((err) => {
|
|
2702
|
+
try {
|
|
2703
|
+
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
2704
|
+
kind: "p2p_dispatch_failed",
|
|
2705
|
+
nodeId: node.id,
|
|
2706
|
+
payload: {
|
|
2707
|
+
source: "queue",
|
|
2708
|
+
via: "p2p_direct",
|
|
2709
|
+
taskId: task.id,
|
|
2710
|
+
error: err?.message || String(err),
|
|
2711
|
+
dispatchFailedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
} catch {
|
|
2715
|
+
}
|
|
1832
2716
|
})
|
|
1833
2717
|
);
|
|
1834
2718
|
}
|
|
1835
2719
|
Promise.all(dispatchPromises).catch(() => {
|
|
1836
2720
|
});
|
|
1837
|
-
return JSON.stringify({
|
|
2721
|
+
return JSON.stringify({
|
|
2722
|
+
success: true,
|
|
2723
|
+
source: "queue",
|
|
2724
|
+
taskId: task.id,
|
|
2725
|
+
status: task.status,
|
|
2726
|
+
taskMode: task.taskMode,
|
|
2727
|
+
requiredTags: task.requiredTags,
|
|
2728
|
+
queueTrigger,
|
|
2729
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
2730
|
+
});
|
|
1838
2731
|
}
|
|
1839
|
-
return JSON.stringify({ success: true, taskId: task.id, status: task.status });
|
|
1840
2732
|
} catch (e) {
|
|
1841
|
-
|
|
2733
|
+
const message = e?.message || String(e);
|
|
2734
|
+
if (message.includes("live_debug_readonly_guardrail_violation")) {
|
|
2735
|
+
return JSON.stringify({ success: false, code: "live_debug_readonly_guardrail_violation", taskMode, error: message });
|
|
2736
|
+
}
|
|
2737
|
+
if (message.includes("dependency_cycle_detected")) {
|
|
2738
|
+
return JSON.stringify({ success: false, code: "dependency_cycle_detected", dependsOn, error: message });
|
|
2739
|
+
}
|
|
2740
|
+
return JSON.stringify({ success: false, error: message });
|
|
1842
2741
|
}
|
|
1843
2742
|
}
|
|
1844
2743
|
async function meshViewQueue(ctx, args) {
|
|
2744
|
+
const rateResult = (0, import_daemon_core.recordMeshToolCall)({ meshId: ctx.mesh.id, tool: "mesh_view_queue" });
|
|
1845
2745
|
try {
|
|
2746
|
+
await refreshMeshFromDaemon(ctx);
|
|
1846
2747
|
const statusFilter = sanitizeQueueStatusFilter(args.status);
|
|
1847
2748
|
const view = normalizeQueueViewMode(args.view);
|
|
1848
|
-
const
|
|
2749
|
+
const rawQueue = (0, import_daemon_core.getQueue)(ctx.mesh.id);
|
|
2750
|
+
const statusById = new Map(rawQueue.map((task) => [task.id, task.status]));
|
|
2751
|
+
const withDependencies = rawQueue.map((task) => {
|
|
2752
|
+
if (!Array.isArray(task.dependsOn) || task.dependsOn.length === 0) return task;
|
|
2753
|
+
const depState = (0, import_daemon_core.describeTaskDependencyState)(task, statusById);
|
|
2754
|
+
return { ...task, ...depState };
|
|
2755
|
+
});
|
|
2756
|
+
const fullQueue = prioritizeActiveQueueRows(annotateQueueStaleness(withDependencies, ctx.mesh));
|
|
1849
2757
|
const queue = filterQueueForView(fullQueue, view, statusFilter);
|
|
1850
2758
|
const summary = buildQueueStatusSummary(fullQueue);
|
|
1851
2759
|
const visibleSummary = buildQueueStatusSummary(queue);
|
|
1852
2760
|
const maintenance = buildQueueMaintenanceReport(fullQueue);
|
|
2761
|
+
const liveNodes = await collectMeshViewQueueNodesWithLiveSessions(ctx);
|
|
2762
|
+
let ledgerEntries = (0, import_daemon_core.readLedgerEntries)(ctx.mesh.id, { tail: 200 });
|
|
2763
|
+
let directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(ctx.mesh.id);
|
|
2764
|
+
const directReconciliation = await reconcileDirectDispatchesFromTranscriptEvidence(ctx, liveNodes, directDispatches, ledgerEntries);
|
|
2765
|
+
if (directReconciliation.reconciled > 0) {
|
|
2766
|
+
ledgerEntries = (0, import_daemon_core.readLedgerEntries)(ctx.mesh.id, { tail: 200 });
|
|
2767
|
+
directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(ctx.mesh.id);
|
|
2768
|
+
}
|
|
2769
|
+
(0, import_daemon_core.markStaleDirectDispatches)(ctx.mesh.id);
|
|
2770
|
+
directDispatches = (0, import_daemon_core.getActiveDirectDispatches)(ctx.mesh.id);
|
|
2771
|
+
const activeWorkEvidence = (0, import_daemon_core.buildMeshActiveWork)({
|
|
2772
|
+
meshId: ctx.mesh.id,
|
|
2773
|
+
queue: fullQueue,
|
|
2774
|
+
ledgerEntries,
|
|
2775
|
+
// Always pass MeshRuntimeStore records (may be empty). buildMeshActiveWork uses them for local
|
|
2776
|
+
// dispatches and falls through to ledger scan for remote P2P dispatches not in MeshRuntimeStore.
|
|
2777
|
+
directDispatches,
|
|
2778
|
+
nodes: liveNodes
|
|
2779
|
+
});
|
|
2780
|
+
const recentDispatchFailures = ledgerEntries.filter((e) => e.kind === "p2p_dispatch_failed").slice(-20).map((e) => ({
|
|
2781
|
+
nodeId: e.nodeId,
|
|
2782
|
+
taskId: e.payload?.taskId,
|
|
2783
|
+
error: e.payload?.error,
|
|
2784
|
+
via: e.payload?.via,
|
|
2785
|
+
failedAt: e.payload?.dispatchFailedAt || e.timestamp
|
|
2786
|
+
}));
|
|
1853
2787
|
const staleAssignedTasks = maintenance.staleAssignedTasks || [];
|
|
1854
2788
|
const requestedHistoricalRows = queue.some((task) => HISTORICAL_QUEUE_STATUSES.has(String(task?.status || "")));
|
|
2789
|
+
const pollingGuidance = buildActiveWorkPollingGuidance(activeWorkEvidence.summary);
|
|
1855
2790
|
return JSON.stringify({
|
|
1856
2791
|
success: true,
|
|
1857
2792
|
sourceOfTruth: {
|
|
@@ -1866,21 +2801,30 @@ async function meshViewQueue(ctx, args) {
|
|
|
1866
2801
|
filtered: Boolean(statusFilter?.length) || view !== "all"
|
|
1867
2802
|
},
|
|
1868
2803
|
queue,
|
|
1869
|
-
|
|
1870
|
-
|
|
2804
|
+
activeWork: activeWorkEvidence.activeWork,
|
|
2805
|
+
staleDirectWork: activeWorkEvidence.staleDirectWork,
|
|
2806
|
+
activeWorkSummary: activeWorkEvidence.summary,
|
|
2807
|
+
...pollingGuidance ? { pollingGuidance } : {},
|
|
2808
|
+
...rateResult.rateLimitExceeded ? { pollingRateAdvisory: { type: "rate_limit_exceeded", tool: "mesh_view_queue", callsInWindow: rateResult.callsInWindow, message: rateResult.advisory } } : {},
|
|
1871
2809
|
summary,
|
|
2810
|
+
visibleSummary,
|
|
1872
2811
|
activeCounts: summary.activeCounts,
|
|
1873
2812
|
historicalCounts: summary.historicalCounts,
|
|
1874
|
-
activeCount: summary.activeCount,
|
|
1875
|
-
historicalCount: summary.historicalCount,
|
|
1876
2813
|
visibleActiveCounts: visibleSummary.activeCounts,
|
|
1877
2814
|
visibleHistoricalCounts: visibleSummary.historicalCounts,
|
|
2815
|
+
activeCount: summary.activeCount,
|
|
2816
|
+
historicalCount: summary.historicalCount,
|
|
1878
2817
|
visibleActiveCount: visibleSummary.activeCount,
|
|
1879
2818
|
visibleHistoricalCount: visibleSummary.historicalCount,
|
|
1880
2819
|
staleAssignedTasks,
|
|
1881
2820
|
staleAssignedCount: maintenance.staleAssignedCount,
|
|
1882
2821
|
queueMaintenance: maintenance,
|
|
1883
2822
|
cleanupDryRun: maintenance,
|
|
2823
|
+
...recentDispatchFailures.length > 0 ? {
|
|
2824
|
+
recentDispatchFailures,
|
|
2825
|
+
dispatchFailureCount: recentDispatchFailures.length,
|
|
2826
|
+
dispatchFailureNote: "Remote P2P dispatch attempts that failed. Affected tasks remain pending and may require mesh_queue_requeue if no idle session picks them up."
|
|
2827
|
+
} : {},
|
|
1884
2828
|
...view === "active" || statusFilter?.some((status) => ACTIVE_QUEUE_STATUSES.has(status)) ? {
|
|
1885
2829
|
activeQueue: queue.filter((task) => ACTIVE_QUEUE_STATUSES.has(String(task?.status || "")))
|
|
1886
2830
|
} : {},
|
|
@@ -1900,6 +2844,8 @@ async function meshQueueCancel(ctx, args) {
|
|
|
1900
2844
|
if (!taskId) return JSON.stringify({ success: false, error: "task_id required" });
|
|
1901
2845
|
const task = (0, import_daemon_core.cancelTask)(ctx.mesh.id, taskId, { reason: args.reason });
|
|
1902
2846
|
if (!task) return JSON.stringify({ success: false, error: `Queue task '${taskId}' not found` });
|
|
2847
|
+
ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
2848
|
+
});
|
|
1903
2849
|
return JSON.stringify({ success: true, task }, null, 2);
|
|
1904
2850
|
} catch (e) {
|
|
1905
2851
|
return JSON.stringify({ success: false, error: e.message });
|
|
@@ -1917,23 +2863,81 @@ async function meshQueueRequeue(ctx, args) {
|
|
|
1917
2863
|
targetNodeId,
|
|
1918
2864
|
targetSessionId,
|
|
1919
2865
|
clearTargetNode: args.clear_target_node === true || args.clearTargetNode === true,
|
|
1920
|
-
clearTargetSession: targetSessionId ? false : !keepTargetSession
|
|
2866
|
+
clearTargetSession: targetSessionId ? false : !keepTargetSession,
|
|
2867
|
+
force: args.force === true
|
|
1921
2868
|
});
|
|
1922
2869
|
if (!task) return JSON.stringify({ success: false, error: `Queue task '${taskId}' not found` });
|
|
1923
|
-
if (
|
|
1924
|
-
|
|
1925
|
-
|
|
2870
|
+
if (task.status === "failed" && task.cancelReason?.startsWith("max_retries_exceeded")) {
|
|
2871
|
+
return JSON.stringify({
|
|
2872
|
+
success: false,
|
|
2873
|
+
code: "max_retries_exceeded",
|
|
2874
|
+
error: task.cancelReason,
|
|
2875
|
+
task,
|
|
2876
|
+
hint: "Use force=true to bypass the retry cap for explicit operator recovery."
|
|
2877
|
+
}, null, 2);
|
|
1926
2878
|
}
|
|
2879
|
+
ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
2880
|
+
});
|
|
1927
2881
|
return JSON.stringify({ success: true, task }, null, 2);
|
|
1928
2882
|
} catch (e) {
|
|
1929
2883
|
return JSON.stringify({ success: false, error: e.message });
|
|
1930
2884
|
}
|
|
1931
2885
|
}
|
|
1932
2886
|
async function meshSendTask(ctx, args) {
|
|
2887
|
+
const requestedTaskMode = readString(args.task_mode) || readString(args.taskMode);
|
|
2888
|
+
const modeValidation = (0, import_daemon_core.validateMeshTaskModeRequest)(requestedTaskMode, args.message);
|
|
2889
|
+
if (!modeValidation.valid) {
|
|
2890
|
+
return JSON.stringify({
|
|
2891
|
+
success: false,
|
|
2892
|
+
code: "live_debug_readonly_guardrail_violation",
|
|
2893
|
+
taskMode: modeValidation.taskMode || requestedTaskMode,
|
|
2894
|
+
violations: modeValidation.violations,
|
|
2895
|
+
allowedOperations: modeValidation.allowedOperations,
|
|
2896
|
+
error: `live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(", ")})`
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
const taskMode = modeValidation.taskMode;
|
|
1933
2900
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
1934
2901
|
if (node.policy?.readOnly) {
|
|
1935
2902
|
return JSON.stringify({ error: `Node '${args.node_id}' is read-only` });
|
|
1936
2903
|
}
|
|
2904
|
+
let explicitTargetSession;
|
|
2905
|
+
if (args.session_id && isWorkerTaskMode(taskMode)) {
|
|
2906
|
+
try {
|
|
2907
|
+
const statusResult = await commandForNode(ctx, node, "get_status_metadata", {});
|
|
2908
|
+
const sessions = extractStatusMetadataSessions(statusResult);
|
|
2909
|
+
explicitTargetSession = sessions.find((session) => readSessionRecordId(session) === args.session_id);
|
|
2910
|
+
if (explicitTargetSession && isMeshCoordinatorSessionRecord(explicitTargetSession)) {
|
|
2911
|
+
return JSON.stringify({
|
|
2912
|
+
success: false,
|
|
2913
|
+
recoverable: true,
|
|
2914
|
+
code: "mesh_target_session_is_coordinator",
|
|
2915
|
+
reason: "mesh_target_session_is_coordinator",
|
|
2916
|
+
nodeId: args.node_id,
|
|
2917
|
+
sessionId: args.session_id,
|
|
2918
|
+
taskMode: taskMode || "unspecified",
|
|
2919
|
+
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.`,
|
|
2920
|
+
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.`
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
if (explicitTargetSession && isUnmanagedSessionRecord(explicitTargetSession)) {
|
|
2924
|
+
return JSON.stringify({
|
|
2925
|
+
success: false,
|
|
2926
|
+
recoverable: true,
|
|
2927
|
+
code: "mesh_target_session_unmanaged",
|
|
2928
|
+
reason: "mesh_target_session_unmanaged",
|
|
2929
|
+
nodeId: args.node_id,
|
|
2930
|
+
sessionId: args.session_id,
|
|
2931
|
+
taskMode: taskMode || "unspecified",
|
|
2932
|
+
unsafeTranscriptAlias: true,
|
|
2933
|
+
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.`,
|
|
2934
|
+
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.`
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
} catch {
|
|
2938
|
+
explicitTargetSession = void 0;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
1937
2941
|
const duplicate = hasRecentDuplicateDispatch(ctx, args);
|
|
1938
2942
|
if (duplicate.duplicate) {
|
|
1939
2943
|
return JSON.stringify({
|
|
@@ -1953,47 +2957,73 @@ async function meshSendTask(ctx, args) {
|
|
|
1953
2957
|
});
|
|
1954
2958
|
}
|
|
1955
2959
|
try {
|
|
1956
|
-
if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
1957
|
-
const res = await ctx.transport.meshEnqueueTask(node.daemonId, {
|
|
1958
|
-
meshId: ctx.mesh.id,
|
|
1959
|
-
message: args.message,
|
|
1960
|
-
targetNodeId: args.node_id
|
|
1961
|
-
});
|
|
1962
|
-
return JSON.stringify(res);
|
|
1963
|
-
}
|
|
1964
2960
|
const isLocalNode = isLocalControlPlaneNode(ctx, node);
|
|
1965
2961
|
if (ctx.transport instanceof IpcTransport && node.daemonId && !isLocalNode) {
|
|
1966
|
-
const cached =
|
|
2962
|
+
const cached = getSessionMetadata(meshSessionCacheKey(args.node_id, args.session_id || ""));
|
|
2963
|
+
const taskId = (0, import_node_crypto.randomUUID)();
|
|
2964
|
+
const coordinatorDaemonId = resolveCoordinatorNode(ctx)?.daemonId || ctx.localDaemonId;
|
|
1967
2965
|
const result2 = await ipcDispatchToRemoteAgent(ctx, node, {
|
|
1968
2966
|
session_id: args.session_id,
|
|
1969
2967
|
message: args.message,
|
|
1970
|
-
providerType: cached?.providerType
|
|
2968
|
+
providerType: cached?.providerType,
|
|
2969
|
+
verifiedSession: explicitTargetSession,
|
|
2970
|
+
meshContext: {
|
|
2971
|
+
meshId: ctx.mesh.id,
|
|
2972
|
+
nodeId: args.node_id,
|
|
2973
|
+
taskId,
|
|
2974
|
+
...coordinatorDaemonId ? { coordinatorDaemonId } : {}
|
|
2975
|
+
}
|
|
1971
2976
|
});
|
|
1972
2977
|
if (result2.success) {
|
|
1973
2978
|
const dispatchedSessionId = args.session_id || result2.sessionId;
|
|
2979
|
+
const dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1974
2980
|
try {
|
|
2981
|
+
const providerType = result2.providerType || cached?.providerType;
|
|
1975
2982
|
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
1976
2983
|
kind: "task_dispatched",
|
|
1977
2984
|
nodeId: args.node_id,
|
|
1978
2985
|
sessionId: dispatchedSessionId,
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2986
|
+
providerType,
|
|
2987
|
+
payload: buildDirectTaskPayload(args.message, "p2p_direct", {
|
|
2988
|
+
taskId,
|
|
2989
|
+
taskMode,
|
|
2990
|
+
providerType,
|
|
2991
|
+
targetSessionId: dispatchedSessionId
|
|
2992
|
+
})
|
|
2993
|
+
});
|
|
2994
|
+
(0, import_daemon_core.insertDirectDispatch)(ctx.mesh.id, {
|
|
2995
|
+
taskId,
|
|
2996
|
+
nodeId: args.node_id,
|
|
2997
|
+
sessionId: dispatchedSessionId,
|
|
2998
|
+
providerType: providerType || void 0,
|
|
2999
|
+
message: args.message,
|
|
3000
|
+
taskMode: taskMode || void 0,
|
|
3001
|
+
via: "p2p_direct",
|
|
3002
|
+
dispatchedAt
|
|
1984
3003
|
});
|
|
1985
3004
|
} catch {
|
|
1986
3005
|
}
|
|
1987
3006
|
}
|
|
1988
|
-
return JSON.stringify({
|
|
3007
|
+
return JSON.stringify({
|
|
3008
|
+
...result2,
|
|
3009
|
+
nodeId: args.node_id,
|
|
3010
|
+
sessionId: result2.success ? args.session_id || result2.sessionId : args.session_id,
|
|
3011
|
+
...result2.success ? { source: "direct", taskId } : {},
|
|
3012
|
+
taskMode,
|
|
3013
|
+
...result2.success && result2.providerType ? { providerType: result2.providerType } : {},
|
|
3014
|
+
dispatched: result2.success === true
|
|
3015
|
+
});
|
|
1989
3016
|
}
|
|
1990
|
-
if (args.session_id
|
|
1991
|
-
const cached =
|
|
3017
|
+
if (args.session_id) {
|
|
3018
|
+
const cached = getSessionMetadata(meshSessionCacheKey(args.node_id, args.session_id));
|
|
1992
3019
|
let resolvedProviderType = cached?.providerType || "";
|
|
1993
3020
|
if (!resolvedProviderType) {
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
3021
|
+
let explicitSession = explicitTargetSession;
|
|
3022
|
+
if (!explicitSession) {
|
|
3023
|
+
const statusResult = await commandForNode(ctx, node, "get_status_metadata", {});
|
|
3024
|
+
const sessions = extractStatusMetadataSessions(statusResult);
|
|
3025
|
+
explicitSession = sessions.find((session) => readSessionRecordId(session) === args.session_id);
|
|
3026
|
+
}
|
|
1997
3027
|
if (!explicitSession) {
|
|
1998
3028
|
return JSON.stringify({
|
|
1999
3029
|
success: false,
|
|
@@ -2008,11 +3038,40 @@ async function meshSendTask(ctx, args) {
|
|
|
2008
3038
|
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.`
|
|
2009
3039
|
});
|
|
2010
3040
|
}
|
|
3041
|
+
if (isMeshCoordinatorSessionRecord(explicitSession)) {
|
|
3042
|
+
return JSON.stringify({
|
|
3043
|
+
success: false,
|
|
3044
|
+
recoverable: true,
|
|
3045
|
+
code: "mesh_target_session_is_coordinator",
|
|
3046
|
+
reason: "mesh_target_session_is_coordinator",
|
|
3047
|
+
nodeId: args.node_id,
|
|
3048
|
+
sessionId: args.session_id,
|
|
3049
|
+
taskMode: taskMode || "unspecified",
|
|
3050
|
+
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.`,
|
|
3051
|
+
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.`
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
if (isUnmanagedSessionRecord(explicitSession)) {
|
|
3055
|
+
return JSON.stringify({
|
|
3056
|
+
success: false,
|
|
3057
|
+
recoverable: true,
|
|
3058
|
+
code: "mesh_target_session_unmanaged",
|
|
3059
|
+
reason: "mesh_target_session_unmanaged",
|
|
3060
|
+
nodeId: args.node_id,
|
|
3061
|
+
sessionId: args.session_id,
|
|
3062
|
+
taskMode: taskMode || "unspecified",
|
|
3063
|
+
unsafeTranscriptAlias: true,
|
|
3064
|
+
unsafeDelegateTarget: true,
|
|
3065
|
+
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.`,
|
|
3066
|
+
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.`
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
2011
3069
|
resolvedProviderType = resolveSessionProviderType(explicitSession);
|
|
2012
3070
|
if (resolvedProviderType) {
|
|
2013
3071
|
meshSessionProviderMetadata.set(meshSessionCacheKey(args.node_id, args.session_id), {
|
|
2014
3072
|
providerType: resolvedProviderType,
|
|
2015
|
-
providerSessionId: readString(explicitSession?.providerSessionId) || void 0
|
|
3073
|
+
providerSessionId: readString(explicitSession?.providerSessionId) || void 0,
|
|
3074
|
+
expiresAt: Date.now() + SESSION_PROVIDER_METADATA_TTL_MS
|
|
2016
3075
|
});
|
|
2017
3076
|
}
|
|
2018
3077
|
}
|
|
@@ -2030,17 +3089,58 @@ async function meshSendTask(ctx, args) {
|
|
|
2030
3089
|
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.`
|
|
2031
3090
|
});
|
|
2032
3091
|
}
|
|
3092
|
+
if (explicitTargetSession && !isIdleSessionRecord(explicitTargetSession) && !isTerminalSessionRecord(explicitTargetSession)) {
|
|
3093
|
+
const sessionStatus = typeof explicitTargetSession?.status === "string" ? explicitTargetSession.status : "unknown";
|
|
3094
|
+
const { createSessionDelivery: createDelivery, resolveDeliveryDecision } = await import("@adhdev/daemon-core");
|
|
3095
|
+
const policyResult = resolveDeliveryDecision(sessionStatus, { kind: "task" });
|
|
3096
|
+
if (policyResult.decision === "queued") {
|
|
3097
|
+
const delivery = createDelivery({
|
|
3098
|
+
meshId: ctx.mesh.id,
|
|
3099
|
+
nodeId: args.node_id,
|
|
3100
|
+
sessionId: args.session_id,
|
|
3101
|
+
providerType: resolvedProviderType,
|
|
3102
|
+
kind: "task",
|
|
3103
|
+
message: args.message,
|
|
3104
|
+
status: "queued"
|
|
3105
|
+
});
|
|
3106
|
+
return JSON.stringify({
|
|
3107
|
+
success: true,
|
|
3108
|
+
dispatched: false,
|
|
3109
|
+
decision: "queued_delivery",
|
|
3110
|
+
deliveryId: delivery.id,
|
|
3111
|
+
reason: policyResult.reason,
|
|
3112
|
+
nodeId: args.node_id,
|
|
3113
|
+
sessionId: args.session_id,
|
|
3114
|
+
sessionStatus,
|
|
3115
|
+
taskMode: taskMode || void 0,
|
|
3116
|
+
message: policyResult.message,
|
|
3117
|
+
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.`
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
const sessionWasIdle = explicitTargetSession ? isIdleSessionRecord(explicitTargetSession) : false;
|
|
3122
|
+
const taskId = (0, import_node_crypto.randomUUID)();
|
|
3123
|
+
const dispatchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3124
|
+
const coordinatorDaemonId = resolveCoordinatorNode(ctx)?.daemonId || ctx.localDaemonId;
|
|
2033
3125
|
const dispatchResult = await commandForNode(ctx, node, "agent_command", {
|
|
2034
3126
|
targetSessionId: args.session_id,
|
|
2035
3127
|
agentType: resolvedProviderType,
|
|
2036
3128
|
cliType: resolvedProviderType,
|
|
2037
3129
|
providerType: resolvedProviderType,
|
|
2038
3130
|
action: "send_chat",
|
|
2039
|
-
message: args.message
|
|
3131
|
+
message: args.message,
|
|
3132
|
+
meshContext: {
|
|
3133
|
+
meshId: ctx.mesh.id,
|
|
3134
|
+
nodeId: args.node_id,
|
|
3135
|
+
taskId,
|
|
3136
|
+
...coordinatorDaemonId ? { coordinatorDaemonId } : {}
|
|
3137
|
+
}
|
|
2040
3138
|
});
|
|
2041
3139
|
const dispatchPayload = unwrapCommandPayload(dispatchResult);
|
|
2042
3140
|
if (dispatchPayload?.success === false || dispatchResult?.success === false) {
|
|
3141
|
+
const source = dispatchPayload?.success === false ? dispatchPayload : dispatchResult;
|
|
2043
3142
|
return JSON.stringify({
|
|
3143
|
+
...source && typeof source === "object" ? source : {},
|
|
2044
3144
|
success: false,
|
|
2045
3145
|
nodeId: args.node_id,
|
|
2046
3146
|
sessionId: args.session_id,
|
|
@@ -2053,22 +3153,78 @@ async function meshSendTask(ctx, args) {
|
|
|
2053
3153
|
nodeId: args.node_id,
|
|
2054
3154
|
sessionId: args.session_id,
|
|
2055
3155
|
providerType: resolvedProviderType,
|
|
2056
|
-
payload:
|
|
3156
|
+
payload: buildDirectTaskPayload(args.message, "local_direct", {
|
|
3157
|
+
taskId,
|
|
3158
|
+
taskMode,
|
|
3159
|
+
providerType: resolvedProviderType,
|
|
3160
|
+
targetSessionId: args.session_id,
|
|
3161
|
+
dispatchedToIdleSession: sessionWasIdle
|
|
3162
|
+
})
|
|
3163
|
+
});
|
|
3164
|
+
} catch {
|
|
3165
|
+
}
|
|
3166
|
+
(0, import_daemon_core.insertDirectDispatch)(ctx.mesh.id, {
|
|
3167
|
+
taskId,
|
|
3168
|
+
nodeId: args.node_id,
|
|
3169
|
+
sessionId: args.session_id,
|
|
3170
|
+
providerType: resolvedProviderType || void 0,
|
|
3171
|
+
message: args.message,
|
|
3172
|
+
taskMode: taskMode || void 0,
|
|
3173
|
+
via: "local_direct",
|
|
3174
|
+
dispatchedToIdleSession: sessionWasIdle,
|
|
3175
|
+
dispatchedAt
|
|
3176
|
+
});
|
|
3177
|
+
let deliveryId;
|
|
3178
|
+
try {
|
|
3179
|
+
const { createSessionDelivery: createDelivery } = await import("@adhdev/daemon-core");
|
|
3180
|
+
const delivery = createDelivery({
|
|
3181
|
+
meshId: ctx.mesh.id,
|
|
3182
|
+
nodeId: args.node_id,
|
|
3183
|
+
sessionId: args.session_id,
|
|
3184
|
+
providerType: resolvedProviderType || void 0,
|
|
3185
|
+
taskId,
|
|
3186
|
+
kind: "task",
|
|
3187
|
+
message: args.message,
|
|
3188
|
+
status: sessionWasIdle ? "delivered" : "delivering"
|
|
2057
3189
|
});
|
|
3190
|
+
deliveryId = delivery.id;
|
|
2058
3191
|
} catch {
|
|
2059
3192
|
}
|
|
2060
|
-
return JSON.stringify({
|
|
3193
|
+
return JSON.stringify({
|
|
3194
|
+
success: true,
|
|
3195
|
+
dispatched: true,
|
|
3196
|
+
decision: "immediate",
|
|
3197
|
+
source: "direct",
|
|
3198
|
+
taskId,
|
|
3199
|
+
deliveryId,
|
|
3200
|
+
taskMode,
|
|
3201
|
+
providerType: resolvedProviderType,
|
|
3202
|
+
nodeId: args.node_id,
|
|
3203
|
+
sessionId: args.session_id,
|
|
3204
|
+
...sessionWasIdle ? {
|
|
3205
|
+
dispatchAcknowledgementRisk: true,
|
|
3206
|
+
dispatchAcknowledgementRiskReason: "session_was_idle_at_dispatch",
|
|
3207
|
+
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.`
|
|
3208
|
+
} : {}
|
|
3209
|
+
});
|
|
2061
3210
|
}
|
|
2062
3211
|
const task = (0, import_daemon_core.enqueueTask)(ctx.mesh.id, args.message, {
|
|
2063
3212
|
targetNodeId: args.node_id,
|
|
2064
|
-
targetSessionId: args.session_id
|
|
3213
|
+
targetSessionId: args.session_id,
|
|
3214
|
+
taskMode
|
|
2065
3215
|
});
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
3216
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx);
|
|
3217
|
+
const pendingEvents = (0, import_daemon_core.drainPendingMeshCoordinatorEvents)(ctx.mesh.id, ctx.localDaemonId);
|
|
3218
|
+
const result = {
|
|
3219
|
+
success: true,
|
|
3220
|
+
source: "queue",
|
|
3221
|
+
nodeId: args.node_id,
|
|
3222
|
+
taskId: task.id,
|
|
3223
|
+
status: task.status,
|
|
3224
|
+
taskMode: task.taskMode,
|
|
3225
|
+
queueTrigger,
|
|
3226
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
3227
|
+
};
|
|
2072
3228
|
if (pendingEvents.length > 0) {
|
|
2073
3229
|
result.pendingCoordinatorEvents = pendingEvents;
|
|
2074
3230
|
}
|
|
@@ -2088,88 +3244,59 @@ async function meshReadChat(ctx, args) {
|
|
|
2088
3244
|
if (!node) {
|
|
2089
3245
|
return JSON.stringify(buildMissingNodeReadChatRecovery(ctx, args), null, 2);
|
|
2090
3246
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
3247
|
+
await drainCoordinatorPendingEvents(ctx, { nodeIds: [args.node_id] });
|
|
3248
|
+
const cached = resolveMeshSessionProviderMetadata(ctx, args.node_id, args.session_id);
|
|
3249
|
+
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
3250
|
+
const result = await commandForNode(ctx, node, "read_chat", {
|
|
3251
|
+
sessionId: args.session_id,
|
|
3252
|
+
targetSessionId: args.session_id,
|
|
3253
|
+
workspace: node.workspace,
|
|
3254
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
3255
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3256
|
+
tailLimit: args.tail ?? 10
|
|
3257
|
+
});
|
|
3258
|
+
const payload = annotateRapidReadChatAdvisory(unwrapCommandPayload(result), {
|
|
3259
|
+
key: `mesh:${args.node_id}:${args.session_id}`,
|
|
3260
|
+
toolName: "mesh_read_chat",
|
|
3261
|
+
completionCallbackExpected: true
|
|
3262
|
+
});
|
|
3263
|
+
const useCompact = args.compact !== false;
|
|
3264
|
+
if (useCompact) {
|
|
3265
|
+
const compactPayload = compactChatPayload(payload, {
|
|
3266
|
+
nodeId: args.node_id,
|
|
2098
3267
|
sessionId: args.session_id,
|
|
2099
|
-
|
|
2100
|
-
workspace: node.workspace,
|
|
2101
|
-
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
2102
|
-
...providerSessionId ? { providerSessionId } : {},
|
|
2103
|
-
tailLimit: args.tail ?? 10
|
|
3268
|
+
limit: args.tail ?? 10
|
|
2104
3269
|
});
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
if (args.compact) {
|
|
2111
|
-
const compactPayload = compactChatPayload(payload, {
|
|
2112
|
-
nodeId: args.node_id,
|
|
2113
|
-
sessionId: args.session_id,
|
|
2114
|
-
limit: args.tail ?? 10
|
|
2115
|
-
});
|
|
2116
|
-
return JSON.stringify(
|
|
2117
|
-
payload.pollingAdvisory ? { ...compactPayload, pollingAdvisory: payload.pollingAdvisory } : compactPayload,
|
|
2118
|
-
null,
|
|
2119
|
-
2
|
|
2120
|
-
);
|
|
2121
|
-
}
|
|
2122
|
-
return JSON.stringify(payload, null, 2);
|
|
2123
|
-
} else if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2124
|
-
try {
|
|
2125
|
-
const targetId = `${node.daemonId}:session:${args.session_id}`;
|
|
2126
|
-
const res = await ctx.transport.readChat(targetId, {
|
|
2127
|
-
limit: args.tail ?? 10,
|
|
2128
|
-
sessionId: args.session_id
|
|
2129
|
-
});
|
|
2130
|
-
return JSON.stringify(res, null, 2);
|
|
2131
|
-
} catch (e) {
|
|
2132
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
2133
|
-
}
|
|
2134
|
-
} else {
|
|
2135
|
-
return JSON.stringify({ error: "Cloud mesh read_chat requires node daemonId" });
|
|
3270
|
+
return JSON.stringify(
|
|
3271
|
+
payload.pollingAdvisory ? { ...compactPayload, pollingAdvisory: payload.pollingAdvisory } : compactPayload,
|
|
3272
|
+
null,
|
|
3273
|
+
2
|
|
3274
|
+
);
|
|
2136
3275
|
}
|
|
3276
|
+
return JSON.stringify(payload, null, 2);
|
|
2137
3277
|
}
|
|
2138
3278
|
async function meshReadDebug(ctx, args) {
|
|
2139
3279
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
return JSON.stringify(payload, null, 2);
|
|
2155
|
-
} else if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2156
|
-
try {
|
|
2157
|
-
const targetId = `${node.daemonId}:session:${args.session_id}`;
|
|
2158
|
-
const res = await ctx.transport.getChatDebugBundle(targetId, {
|
|
2159
|
-
sessionId: args.session_id,
|
|
2160
|
-
tailLimit: args.tail ?? 40,
|
|
2161
|
-
delivery: args.delivery
|
|
2162
|
-
});
|
|
2163
|
-
return JSON.stringify(res, null, 2);
|
|
2164
|
-
} catch (e) {
|
|
2165
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
return JSON.stringify({ error: "Cloud mesh read_debug requires node daemonId" });
|
|
3280
|
+
const cached = resolveMeshSessionProviderMetadata(ctx, args.node_id, args.session_id);
|
|
3281
|
+
const providerSessionId = typeof args.provider_session_id === "string" && args.provider_session_id.trim() ? args.provider_session_id.trim() : cached?.providerSessionId;
|
|
3282
|
+
const delivery = args.delivery === "inline" ? void 0 : "daemon_file";
|
|
3283
|
+
const result = await commandForNode(ctx, node, "get_chat_debug_bundle", {
|
|
3284
|
+
sessionId: args.session_id,
|
|
3285
|
+
targetSessionId: args.session_id,
|
|
3286
|
+
workspace: node.workspace,
|
|
3287
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
3288
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3289
|
+
tailLimit: args.tail ?? 40,
|
|
3290
|
+
...delivery ? { delivery } : {}
|
|
3291
|
+
});
|
|
3292
|
+
const payload = unwrapCommandPayload(result);
|
|
3293
|
+
return JSON.stringify(payload, null, 2);
|
|
2169
3294
|
}
|
|
2170
3295
|
async function meshLaunchSession(ctx, args) {
|
|
2171
3296
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2172
|
-
|
|
3297
|
+
const bootstrapBlock = getWorktreeBootstrapLaunchBlock(node, ctx.mesh.policy);
|
|
3298
|
+
if (bootstrapBlock) return JSON.stringify(bootstrapBlock, null, 2);
|
|
3299
|
+
{
|
|
2173
3300
|
let resolvedProviderType = typeof args.type === "string" && args.type.trim() ? args.type : "";
|
|
2174
3301
|
if (!resolvedProviderType) {
|
|
2175
3302
|
const providerPriority = readProviderPriority(node.policy);
|
|
@@ -2203,6 +3330,9 @@ async function meshLaunchSession(ctx, args) {
|
|
|
2203
3330
|
cliType: resolvedProviderType,
|
|
2204
3331
|
dir: node.workspace,
|
|
2205
3332
|
settings: {
|
|
3333
|
+
// Worker launch envelope (A5): structured metadata so worker sessions
|
|
3334
|
+
// know their role and can route completion events back correctly.
|
|
3335
|
+
role: "worker",
|
|
2206
3336
|
meshNodeFor: ctx.mesh.id,
|
|
2207
3337
|
meshNodeId: args.node_id,
|
|
2208
3338
|
spawnedSessionVisibility,
|
|
@@ -2224,7 +3354,8 @@ async function meshLaunchSession(ctx, args) {
|
|
|
2224
3354
|
if (runtimeSessionId) {
|
|
2225
3355
|
meshSessionProviderMetadata.set(meshSessionCacheKey(args.node_id, runtimeSessionId), {
|
|
2226
3356
|
providerType: resolvedProviderType,
|
|
2227
|
-
...providerSessionId ? { providerSessionId } : {}
|
|
3357
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3358
|
+
expiresAt: Date.now() + SESSION_PROVIDER_METADATA_TTL_MS
|
|
2228
3359
|
});
|
|
2229
3360
|
}
|
|
2230
3361
|
try {
|
|
@@ -2237,63 +3368,14 @@ async function meshLaunchSession(ctx, args) {
|
|
|
2237
3368
|
});
|
|
2238
3369
|
} catch {
|
|
2239
3370
|
}
|
|
2240
|
-
|
|
2241
|
-
ctx.transport.meshCommand(node.daemonId, "trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
2242
|
-
});
|
|
2243
|
-
} else if (isLocalTransport(ctx.transport)) {
|
|
2244
|
-
ctx.transport.command("trigger_mesh_queue", { meshId: ctx.mesh.id }).catch(() => {
|
|
2245
|
-
});
|
|
2246
|
-
}
|
|
3371
|
+
const queueTrigger = await triggerMeshQueueAndReport(ctx, node, { localNode: isLocalNode });
|
|
2247
3372
|
return JSON.stringify({
|
|
2248
3373
|
...launchPayload,
|
|
2249
3374
|
resolvedProviderType,
|
|
2250
|
-
...providerSessionId ? { providerSessionId } : {}
|
|
3375
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3376
|
+
queueTrigger,
|
|
3377
|
+
...buildQueueTriggerGuidance(queueTrigger)
|
|
2251
3378
|
}, null, 2);
|
|
2252
|
-
} else if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2253
|
-
let resolvedProviderType = typeof args.type === "string" && args.type.trim() ? args.type : "";
|
|
2254
|
-
if (!resolvedProviderType) {
|
|
2255
|
-
const providerPriority = readProviderPriority(node.policy);
|
|
2256
|
-
if (!providerPriority.length) {
|
|
2257
|
-
return JSON.stringify({ success: false, error: missingProviderPriorityMessage(args.node_id) });
|
|
2258
|
-
}
|
|
2259
|
-
resolvedProviderType = providerPriority[0];
|
|
2260
|
-
}
|
|
2261
|
-
const coordinatorNode = resolveCoordinatorNode(ctx);
|
|
2262
|
-
const coordinatorDaemonId = coordinatorNode?.daemonId || ctx.localDaemonId;
|
|
2263
|
-
const spawnedSessionVisibility = readSpawnedSessionVisibility(ctx.mesh.policy);
|
|
2264
|
-
if (!coordinatorDaemonId) {
|
|
2265
|
-
return JSON.stringify(buildMissingCoordinatorDaemonIdFailure(ctx, node, resolvedProviderType), null, 2);
|
|
2266
|
-
}
|
|
2267
|
-
try {
|
|
2268
|
-
const res = await ctx.transport.launch(node.daemonId, {
|
|
2269
|
-
type: resolvedProviderType,
|
|
2270
|
-
dir: node.workspace,
|
|
2271
|
-
settings: {
|
|
2272
|
-
meshNodeFor: ctx.mesh.id,
|
|
2273
|
-
meshNodeId: args.node_id,
|
|
2274
|
-
spawnedSessionVisibility,
|
|
2275
|
-
...coordinatorDaemonId ? { meshCoordinatorDaemonId: coordinatorDaemonId } : {},
|
|
2276
|
-
...coordinatorNode?.id ? { meshCoordinatorNodeId: coordinatorNode.id } : {},
|
|
2277
|
-
launchedByCoordinator: true
|
|
2278
|
-
}
|
|
2279
|
-
});
|
|
2280
|
-
const runtimeSessionId = typeof res?.sessionId === "string" ? res.sessionId : typeof res?.id === "string" ? res.id : "";
|
|
2281
|
-
try {
|
|
2282
|
-
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
2283
|
-
kind: "session_launched",
|
|
2284
|
-
nodeId: args.node_id,
|
|
2285
|
-
sessionId: runtimeSessionId || void 0,
|
|
2286
|
-
providerType: resolvedProviderType,
|
|
2287
|
-
payload: {}
|
|
2288
|
-
});
|
|
2289
|
-
} catch {
|
|
2290
|
-
}
|
|
2291
|
-
return JSON.stringify({ ...res, resolvedProviderType }, null, 2);
|
|
2292
|
-
} catch (e) {
|
|
2293
|
-
return JSON.stringify(recordRecoverableLaunchFailure(ctx, node, resolvedProviderType, e), null, 2);
|
|
2294
|
-
}
|
|
2295
|
-
} else {
|
|
2296
|
-
return JSON.stringify({ error: "Cloud mesh launch_session requires node daemonId" });
|
|
2297
3379
|
}
|
|
2298
3380
|
}
|
|
2299
3381
|
async function meshGitStatus(ctx, args) {
|
|
@@ -2301,37 +3383,23 @@ async function meshGitStatus(ctx, args) {
|
|
|
2301
3383
|
const autoDiscoverSubmodules = node.policy?.autoDiscoverSubmodules !== false;
|
|
2302
3384
|
const submoduleIgnorePaths = node.policy?.submoduleIgnorePaths || [];
|
|
2303
3385
|
try {
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
const diffResult = await commandForNode(ctx, node, "git_diff_summary", {
|
|
2322
|
-
workspace: node.workspace
|
|
2323
|
-
});
|
|
2324
|
-
return JSON.stringify({
|
|
2325
|
-
nodeId: args.node_id,
|
|
2326
|
-
workspace: node.workspace,
|
|
2327
|
-
status: extractGitStatus(statusResult),
|
|
2328
|
-
diff: extractGitDiff(diffResult),
|
|
2329
|
-
submodules: autoDiscoverSubmodules ? extractSubmodules(statusResult, submoduleIgnorePaths) : void 0,
|
|
2330
|
-
relatedRepos: await collectRelatedRepoStatuses(ctx, node)
|
|
2331
|
-
}, null, 2);
|
|
2332
|
-
} else {
|
|
2333
|
-
return JSON.stringify({ error: "No daemonId available for cloud git_status probe" });
|
|
2334
|
-
}
|
|
3386
|
+
const statusResult = await commandForNode(ctx, node, "git_status", {
|
|
3387
|
+
workspace: node.workspace,
|
|
3388
|
+
refreshUpstream: true,
|
|
3389
|
+
includeSubmodules: autoDiscoverSubmodules,
|
|
3390
|
+
submoduleIgnorePaths: submoduleIgnorePaths.length > 0 ? submoduleIgnorePaths : void 0
|
|
3391
|
+
});
|
|
3392
|
+
const diffResult = await commandForNode(ctx, node, "git_diff_summary", {
|
|
3393
|
+
workspace: node.workspace
|
|
3394
|
+
});
|
|
3395
|
+
return JSON.stringify({
|
|
3396
|
+
nodeId: args.node_id,
|
|
3397
|
+
workspace: node.workspace,
|
|
3398
|
+
status: extractGitStatus(statusResult),
|
|
3399
|
+
diff: extractGitDiff(diffResult),
|
|
3400
|
+
submodules: autoDiscoverSubmodules ? extractSubmodules(statusResult, submoduleIgnorePaths) : void 0,
|
|
3401
|
+
relatedRepos: await collectRelatedRepoStatuses(ctx, node)
|
|
3402
|
+
}, null, 2);
|
|
2335
3403
|
} catch (e) {
|
|
2336
3404
|
const failure = buildCoordinatorP2pRelayFailure(e, {
|
|
2337
3405
|
command: "git_status",
|
|
@@ -2344,242 +3412,216 @@ async function meshGitStatus(ctx, args) {
|
|
|
2344
3412
|
}, null, 2);
|
|
2345
3413
|
}
|
|
2346
3414
|
}
|
|
2347
|
-
async function
|
|
3415
|
+
async function meshFastForwardNode(ctx, args) {
|
|
3416
|
+
await refreshMeshFromDaemon(ctx);
|
|
2348
3417
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3418
|
+
const submoduleIgnorePaths = node.policy?.submoduleIgnorePaths || [];
|
|
2349
3419
|
if (node.policy?.readOnly) {
|
|
2350
|
-
return JSON.stringify({
|
|
3420
|
+
return JSON.stringify({
|
|
3421
|
+
success: false,
|
|
3422
|
+
code: "node_read_only",
|
|
3423
|
+
nodeId: args.node_id,
|
|
3424
|
+
workspace: node.workspace,
|
|
3425
|
+
allowed: false,
|
|
3426
|
+
willRun: false,
|
|
3427
|
+
executed: false,
|
|
3428
|
+
blockingReasons: ["node_read_only"]
|
|
3429
|
+
}, null, 2);
|
|
2351
3430
|
}
|
|
2352
|
-
|
|
2353
|
-
const
|
|
3431
|
+
try {
|
|
3432
|
+
const dryRun = args.dry_run === true || args.execute !== true;
|
|
3433
|
+
const result = await commandForNode(ctx, node, "fast_forward_mesh_node", {
|
|
3434
|
+
meshId: ctx.mesh.id,
|
|
3435
|
+
nodeId: node.id,
|
|
2354
3436
|
workspace: node.workspace,
|
|
2355
|
-
|
|
2356
|
-
|
|
3437
|
+
branch: typeof args.branch === "string" ? args.branch : void 0,
|
|
3438
|
+
execute: args.execute === true && args.dry_run !== true,
|
|
3439
|
+
dryRun,
|
|
3440
|
+
updateSubmodules: args.update_submodules === true,
|
|
3441
|
+
submoduleIgnorePaths: submoduleIgnorePaths.length > 0 ? submoduleIgnorePaths : void 0
|
|
2357
3442
|
});
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
3443
|
+
return JSON.stringify(unwrapCommandPayload(result), null, 2);
|
|
3444
|
+
} catch (e) {
|
|
3445
|
+
const failure = buildCoordinatorP2pRelayFailure(e, {
|
|
3446
|
+
command: "fast_forward_mesh_node",
|
|
3447
|
+
targetDaemonId: node.daemonId,
|
|
3448
|
+
nodeId: args.node_id
|
|
3449
|
+
});
|
|
3450
|
+
return JSON.stringify({
|
|
3451
|
+
...failure,
|
|
3452
|
+
workspace: node.workspace,
|
|
3453
|
+
allowed: false,
|
|
3454
|
+
willRun: false,
|
|
3455
|
+
executed: false,
|
|
3456
|
+
blockingReasons: [failure.code || "mesh_fast_forward_unavailable"]
|
|
3457
|
+
}, null, 2);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
async function meshCheckpoint(ctx, args) {
|
|
3461
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3462
|
+
if (node.policy?.readOnly) {
|
|
3463
|
+
return JSON.stringify({ error: `Node '${args.node_id}' is read-only \u2014 cannot checkpoint` });
|
|
3464
|
+
}
|
|
3465
|
+
const result = await commandForNode(ctx, node, "git_checkpoint", {
|
|
3466
|
+
workspace: node.workspace,
|
|
3467
|
+
message: args.message,
|
|
3468
|
+
includeUntracked: true
|
|
3469
|
+
});
|
|
3470
|
+
try {
|
|
3471
|
+
(0, import_daemon_core.appendLedgerEntry)(ctx.mesh.id, {
|
|
3472
|
+
kind: "checkpoint_created",
|
|
3473
|
+
nodeId: args.node_id,
|
|
3474
|
+
payload: {
|
|
2371
3475
|
message: args.message,
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
kind: "checkpoint_created",
|
|
2377
|
-
nodeId: args.node_id,
|
|
2378
|
-
payload: { message: args.message, commit: res?.checkpoint?.commit }
|
|
2379
|
-
});
|
|
2380
|
-
} catch {
|
|
3476
|
+
commit: result?.checkpoint?.commit,
|
|
3477
|
+
outcome: result?.checkpoint?.status || (result?.checkpoint?.noop ? "skipped" : void 0),
|
|
3478
|
+
noop: result?.checkpoint?.noop === true,
|
|
3479
|
+
reason: result?.checkpoint?.reason
|
|
2381
3480
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
2385
|
-
}
|
|
2386
|
-
} else {
|
|
2387
|
-
return JSON.stringify({ error: "Cloud mesh checkpoint requires node daemonId" });
|
|
3481
|
+
});
|
|
3482
|
+
} catch {
|
|
2388
3483
|
}
|
|
3484
|
+
return JSON.stringify(result, null, 2);
|
|
2389
3485
|
}
|
|
2390
3486
|
async function meshApprove(ctx, args) {
|
|
2391
3487
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
return JSON.stringify(result, null, 2);
|
|
2404
|
-
} else if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2405
|
-
try {
|
|
2406
|
-
const targetId = `${node.daemonId}:session:${args.session_id}`;
|
|
2407
|
-
const res = await ctx.transport.approve(targetId, args.action === "reject" ? "reject" : "approve");
|
|
2408
|
-
return JSON.stringify(res, null, 2);
|
|
2409
|
-
} catch (e) {
|
|
2410
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
2411
|
-
}
|
|
2412
|
-
} else {
|
|
2413
|
-
return JSON.stringify({ error: "Cloud mesh approve requires node daemonId" });
|
|
2414
|
-
}
|
|
3488
|
+
const cached = getSessionMetadata(meshSessionCacheKey(args.node_id, args.session_id));
|
|
3489
|
+
const providerSessionId = cached?.providerSessionId;
|
|
3490
|
+
const result = await commandForNode(ctx, node, "resolve_action", {
|
|
3491
|
+
sessionId: args.session_id,
|
|
3492
|
+
targetSessionId: args.session_id,
|
|
3493
|
+
workspace: node.workspace,
|
|
3494
|
+
...cached?.providerType ? { agentType: cached.providerType, providerType: cached.providerType } : {},
|
|
3495
|
+
...providerSessionId ? { providerSessionId } : {},
|
|
3496
|
+
action: args.action === "reject" ? "reject" : "approve"
|
|
3497
|
+
});
|
|
3498
|
+
return JSON.stringify(result, null, 2);
|
|
2415
3499
|
}
|
|
2416
3500
|
async function meshCloneNode(ctx, args) {
|
|
2417
3501
|
const sourceNode = await findNodeWithRefresh(ctx, args.source_node_id);
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
await syncCoordinatorDaemonMeshCache(ctx);
|
|
2433
|
-
}
|
|
2434
|
-
return JSON.stringify(result, null, 2);
|
|
2435
|
-
} else if (!isLocalTransport(ctx.transport) && sourceNode.daemonId) {
|
|
2436
|
-
try {
|
|
2437
|
-
const res = await ctx.transport.meshCloneNode(sourceNode.daemonId, {
|
|
2438
|
-
meshId: ctx.mesh.id,
|
|
2439
|
-
sourceNodeId: args.source_node_id,
|
|
2440
|
-
branch: args.branch,
|
|
2441
|
-
baseBranch: args.base_branch,
|
|
2442
|
-
inlineMesh: ctx.mesh
|
|
2443
|
-
});
|
|
2444
|
-
const clonePayload = extractCloneNodePayload(res);
|
|
2445
|
-
if (clonePayload?.success && clonePayload.node?.id) {
|
|
2446
|
-
const existingIndex = ctx.mesh.nodes.findIndex((n) => n.id === clonePayload.node.id);
|
|
2447
|
-
if (existingIndex >= 0) ctx.mesh.nodes[existingIndex] = clonePayload.node;
|
|
2448
|
-
else ctx.mesh.nodes.push(clonePayload.node);
|
|
2449
|
-
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2450
|
-
await syncCoordinatorDaemonMeshCache(ctx);
|
|
2451
|
-
}
|
|
2452
|
-
return JSON.stringify(res, null, 2);
|
|
2453
|
-
} catch (e) {
|
|
2454
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
2455
|
-
}
|
|
2456
|
-
} else {
|
|
2457
|
-
return JSON.stringify({ error: "Cloud mesh clone_node requires source node daemonId" });
|
|
3502
|
+
const result = await commandForNode(ctx, sourceNode, "clone_mesh_node", {
|
|
3503
|
+
meshId: ctx.mesh.id,
|
|
3504
|
+
sourceNodeId: args.source_node_id,
|
|
3505
|
+
branch: args.branch,
|
|
3506
|
+
baseBranch: args.base_branch,
|
|
3507
|
+
inlineMesh: ctx.mesh
|
|
3508
|
+
});
|
|
3509
|
+
const clonePayload = extractCloneNodePayload(result);
|
|
3510
|
+
if (clonePayload?.success && clonePayload.node?.id) {
|
|
3511
|
+
const existingIndex = ctx.mesh.nodes.findIndex((n) => n.id === clonePayload.node.id);
|
|
3512
|
+
if (existingIndex >= 0) ctx.mesh.nodes[existingIndex] = clonePayload.node;
|
|
3513
|
+
else ctx.mesh.nodes.push(clonePayload.node);
|
|
3514
|
+
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3515
|
+
await syncCoordinatorDaemonMeshCache(ctx);
|
|
2458
3516
|
}
|
|
3517
|
+
return JSON.stringify(result, null, 2);
|
|
2459
3518
|
}
|
|
2460
3519
|
async function meshCleanupSessions(ctx, args) {
|
|
2461
3520
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
return JSON.stringify(result, null, 2);
|
|
2472
|
-
} else if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2473
|
-
try {
|
|
2474
|
-
const res = await ctx.transport.meshCleanupSessions(node.daemonId, {
|
|
2475
|
-
meshId: ctx.mesh.id,
|
|
2476
|
-
nodeId: args.node_id,
|
|
2477
|
-
mode: args.mode,
|
|
2478
|
-
sessionIds: args.session_ids,
|
|
2479
|
-
dryRun: args.dry_run === true,
|
|
2480
|
-
inlineMesh: ctx.mesh
|
|
2481
|
-
});
|
|
2482
|
-
return JSON.stringify(res, null, 2);
|
|
2483
|
-
} catch (e) {
|
|
2484
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
2485
|
-
}
|
|
2486
|
-
} else {
|
|
2487
|
-
return JSON.stringify({ error: "Cloud mesh cleanup_sessions requires node daemonId" });
|
|
2488
|
-
}
|
|
3521
|
+
const result = await commandForNode(ctx, node, "cleanup_mesh_sessions", {
|
|
3522
|
+
meshId: ctx.mesh.id,
|
|
3523
|
+
nodeId: args.node_id,
|
|
3524
|
+
mode: args.mode,
|
|
3525
|
+
sessionIds: args.session_ids,
|
|
3526
|
+
dryRun: args.dry_run === true,
|
|
3527
|
+
inlineMesh: ctx.mesh
|
|
3528
|
+
});
|
|
3529
|
+
return JSON.stringify(result, null, 2);
|
|
2489
3530
|
}
|
|
2490
3531
|
async function meshRemoveNode(ctx, args) {
|
|
2491
3532
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
}, null, 2);
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
if (result?.success && result.removed !== false) {
|
|
2516
|
-
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
2517
|
-
if (idx >= 0) {
|
|
2518
|
-
ctx.mesh.nodes.splice(idx, 1);
|
|
2519
|
-
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2520
|
-
}
|
|
3533
|
+
const removeArgs = buildRemoveNodeArgs(ctx, args.node_id, args.session_cleanup_mode);
|
|
3534
|
+
let result;
|
|
3535
|
+
let transportFallback;
|
|
3536
|
+
try {
|
|
3537
|
+
result = await commandForNode(ctx, node, "remove_mesh_node", removeArgs);
|
|
3538
|
+
} catch (e) {
|
|
3539
|
+
if (ctx.transport instanceof IpcTransport && node.isLocalWorktree && isP2pTransportUnavailableError(e)) {
|
|
3540
|
+
result = await ctx.transport.command("remove_mesh_node", removeArgs);
|
|
3541
|
+
transportFallback = {
|
|
3542
|
+
from: "p2p_mesh_relay",
|
|
3543
|
+
to: "local_control_plane",
|
|
3544
|
+
reason: e?.message || String(e)
|
|
3545
|
+
};
|
|
3546
|
+
} else {
|
|
3547
|
+
return JSON.stringify({
|
|
3548
|
+
success: false,
|
|
3549
|
+
code: isP2pTransportUnavailableError(e) ? "p2p_unavailable" : "mesh_remove_node_failed",
|
|
3550
|
+
error: e?.message || String(e),
|
|
3551
|
+
recoveryHint: isP2pTransportUnavailableError(e) ? "If this is an ADHDev-managed local worktree, retry from a coordinator connected to the daemon that owns the worktree; dashboard command/data-plane traffic still requires P2P." : "Inspect mesh_status and retry after resolving the reported failure."
|
|
3552
|
+
}, null, 2);
|
|
2521
3553
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
...args.session_cleanup_mode ? { sessionCleanupMode: args.session_cleanup_mode } : {},
|
|
2529
|
-
inlineMesh: ctx.mesh
|
|
2530
|
-
});
|
|
2531
|
-
if (res?.success && res.removed !== false) {
|
|
2532
|
-
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
2533
|
-
if (idx >= 0) {
|
|
2534
|
-
ctx.mesh.nodes.splice(idx, 1);
|
|
2535
|
-
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2538
|
-
return JSON.stringify(res, null, 2);
|
|
2539
|
-
} catch (e) {
|
|
2540
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
3554
|
+
}
|
|
3555
|
+
if (result?.success && result.removed !== false) {
|
|
3556
|
+
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
3557
|
+
if (idx >= 0) {
|
|
3558
|
+
ctx.mesh.nodes.splice(idx, 1);
|
|
3559
|
+
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2541
3560
|
}
|
|
2542
|
-
} else {
|
|
2543
|
-
return JSON.stringify({ error: "Cloud mesh remove_node requires node daemonId" });
|
|
2544
3561
|
}
|
|
3562
|
+
return JSON.stringify({ ...result || {}, ...transportFallback ? { transportFallback } : {} }, null, 2);
|
|
3563
|
+
}
|
|
3564
|
+
function resolveRefineConfigNode(ctx, nodeId) {
|
|
3565
|
+
if (nodeId) return findNode(ctx.mesh, nodeId);
|
|
3566
|
+
const node = ctx.mesh.nodes.find((entry) => !!entry.workspace);
|
|
3567
|
+
if (!node) throw new Error("No mesh node with a workspace is available");
|
|
3568
|
+
return node;
|
|
3569
|
+
}
|
|
3570
|
+
async function meshRefineConfigSchema(ctx) {
|
|
3571
|
+
const node = resolveRefineConfigNode(ctx);
|
|
3572
|
+
const result = await commandForNode(ctx, node, "get_mesh_refine_config_schema", {});
|
|
3573
|
+
return JSON.stringify(result, null, 2);
|
|
3574
|
+
}
|
|
3575
|
+
async function meshValidateRefineConfig(ctx, args) {
|
|
3576
|
+
const node = resolveRefineConfigNode(ctx, args.node_id);
|
|
3577
|
+
const result = await commandForNode(ctx, node, "validate_mesh_refine_config", {
|
|
3578
|
+
workspace: node.workspace,
|
|
3579
|
+
inlineMesh: ctx.mesh,
|
|
3580
|
+
...args.config ? { config: args.config } : {}
|
|
3581
|
+
});
|
|
3582
|
+
return JSON.stringify(result, null, 2);
|
|
3583
|
+
}
|
|
3584
|
+
async function meshSuggestRefineConfig(ctx, args) {
|
|
3585
|
+
const node = resolveRefineConfigNode(ctx, args.node_id);
|
|
3586
|
+
const result = await commandForNode(ctx, node, "suggest_mesh_refine_config", {
|
|
3587
|
+
workspace: node.workspace,
|
|
3588
|
+
inlineMesh: ctx.mesh
|
|
3589
|
+
});
|
|
3590
|
+
return JSON.stringify(result, null, 2);
|
|
3591
|
+
}
|
|
3592
|
+
async function meshRefinePlan(ctx, args) {
|
|
3593
|
+
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
3594
|
+
const result = await commandForNode(ctx, node, "plan_mesh_refine_node", {
|
|
3595
|
+
meshId: ctx.mesh.id,
|
|
3596
|
+
nodeId: args.node_id,
|
|
3597
|
+
inlineMesh: ctx.mesh
|
|
3598
|
+
});
|
|
3599
|
+
return JSON.stringify(result, null, 2);
|
|
2545
3600
|
}
|
|
2546
3601
|
async function meshRefineNode(ctx, args) {
|
|
2547
3602
|
const node = await findNodeWithRefresh(ctx, args.node_id);
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
return JSON.stringify(result, null, 2);
|
|
2562
|
-
} else if (!isLocalTransport(ctx.transport) && node.daemonId) {
|
|
2563
|
-
try {
|
|
2564
|
-
const res = await ctx.transport.meshRefineNode(node.daemonId, {
|
|
2565
|
-
meshId: ctx.mesh.id,
|
|
2566
|
-
nodeId: args.node_id,
|
|
2567
|
-
inlineMesh: ctx.mesh
|
|
2568
|
-
});
|
|
2569
|
-
if (res?.success && res.removeResult?.removed !== false) {
|
|
2570
|
-
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
2571
|
-
if (idx >= 0) {
|
|
2572
|
-
ctx.mesh.nodes.splice(idx, 1);
|
|
2573
|
-
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
return JSON.stringify(res, null, 2);
|
|
2577
|
-
} catch (e) {
|
|
2578
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
3603
|
+
const result = await commandForNode(ctx, node, "refine_mesh_node", {
|
|
3604
|
+
meshId: ctx.mesh.id,
|
|
3605
|
+
nodeId: args.node_id,
|
|
3606
|
+
inlineMesh: ctx.mesh
|
|
3607
|
+
});
|
|
3608
|
+
if (result?.success && result.async !== true && result.removeResult?.removed !== false) {
|
|
3609
|
+
const idx = ctx.mesh.nodes.findIndex((n) => n.id === args.node_id);
|
|
3610
|
+
if (idx >= 0) {
|
|
3611
|
+
ctx.mesh.nodes.splice(idx, 1);
|
|
3612
|
+
ctx.mesh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2579
3613
|
}
|
|
2580
|
-
} else {
|
|
2581
|
-
return JSON.stringify({ error: "Cloud mesh refine_node requires node daemonId" });
|
|
2582
3614
|
}
|
|
3615
|
+
return JSON.stringify(result, null, 2);
|
|
3616
|
+
}
|
|
3617
|
+
async function meshReviewInbox(ctx, args = {}) {
|
|
3618
|
+
await refreshMeshFromDaemon(ctx);
|
|
3619
|
+
const meshId = (args.mesh_id ?? ctx.mesh.id).trim();
|
|
3620
|
+
const result = await commandForNode(ctx, ctx.mesh.nodes[0], "get_mesh_review_inbox", {
|
|
3621
|
+
meshId,
|
|
3622
|
+
inlineMesh: ctx.mesh
|
|
3623
|
+
});
|
|
3624
|
+
return JSON.stringify(result, null, 2);
|
|
2583
3625
|
}
|
|
2584
3626
|
|
|
2585
3627
|
// src/help.ts
|
|
@@ -2603,28 +3645,24 @@ var STANDARD_TOOLS = [
|
|
|
2603
3645
|
function buildMcpHelpText() {
|
|
2604
3646
|
const meshTools = ALL_MESH_TOOLS.map((tool) => tool.name);
|
|
2605
3647
|
return `
|
|
2606
|
-
|
|
3648
|
+
ADHDev MCP Server
|
|
2607
3649
|
|
|
2608
3650
|
Usage:
|
|
2609
|
-
adhdev
|
|
2610
|
-
adhdev
|
|
2611
|
-
adhdev-mcp --
|
|
2612
|
-
adhdev-mcp --repo-mesh <mesh_id> Mesh mode (coordinator-scoped tools)
|
|
3651
|
+
adhdev mcp Local mode (requires standalone daemon)
|
|
3652
|
+
adhdev mcp --mode ipc --repo-mesh <mesh_id> Cloud daemon IPC mesh mode
|
|
3653
|
+
adhdev-mcp --help Compatibility bin (same server, legacy package entrypoint)
|
|
2613
3654
|
|
|
2614
3655
|
Options:
|
|
2615
|
-
--mode <mode> Transport: local
|
|
3656
|
+
--mode <mode> Transport: local or ipc
|
|
2616
3657
|
--port <n> Standalone or IPC daemon port (defaults: local 3847, ipc 19222)
|
|
2617
3658
|
--password <pass> Standalone daemon password (if set)
|
|
2618
|
-
--api-key <key> ADHDev cloud API key (switches to cloud mode)
|
|
2619
|
-
--base-url <url> Override cloud API base URL
|
|
2620
3659
|
--repo-mesh <mesh_id> Enable mesh mode \u2014 exposes only mesh-scoped coordinator tools
|
|
2621
3660
|
--help Show this help
|
|
2622
3661
|
|
|
2623
3662
|
Environment variables:
|
|
2624
|
-
ADHDEV_API_KEY API key (cloud mode)
|
|
2625
3663
|
ADHDEV_PASSWORD Daemon password (local mode)
|
|
2626
3664
|
ADHDEV_MESH_ID Mesh ID (mesh mode)
|
|
2627
|
-
ADHDEV_MCP_TRANSPORT Transport: local
|
|
3665
|
+
ADHDEV_MCP_TRANSPORT Transport: local or ipc
|
|
2628
3666
|
|
|
2629
3667
|
Standard tools: ${STANDARD_TOOLS.join(", ")}
|
|
2630
3668
|
Mesh tools: ${meshTools.join(", ")}
|
|
@@ -2634,6 +3672,7 @@ Mesh tools: ${meshTools.join(", ")}
|
|
|
2634
3672
|
// src/server.ts
|
|
2635
3673
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
2636
3674
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3675
|
+
var import_node_os = __toESM(require("os"));
|
|
2637
3676
|
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
2638
3677
|
|
|
2639
3678
|
// src/transports/local.ts
|
|
@@ -2652,287 +3691,24 @@ var LocalTransport = class {
|
|
|
2652
3691
|
}
|
|
2653
3692
|
async getStatus() {
|
|
2654
3693
|
const res = await fetch(`${this.baseUrl}/api/v1/status`, { headers: this.headers() });
|
|
2655
|
-
if (!res.ok) throw new Error(`Status fetch failed: ${res.status}`);
|
|
2656
|
-
return res.json();
|
|
2657
|
-
}
|
|
2658
|
-
async command(type, args = {}) {
|
|
2659
|
-
const res = await fetch(`${this.baseUrl}/api/v1/command`, {
|
|
2660
|
-
method: "POST",
|
|
2661
|
-
headers: this.headers(),
|
|
2662
|
-
body: JSON.stringify({ type, ...args })
|
|
2663
|
-
});
|
|
2664
|
-
if (!res.ok) {
|
|
2665
|
-
const text = await res.text().catch(() => res.statusText);
|
|
2666
|
-
throw new Error(`Command ${type} failed: ${res.status} ${text}`);
|
|
2667
|
-
}
|
|
2668
|
-
return res.json();
|
|
2669
|
-
}
|
|
2670
|
-
async ping() {
|
|
2671
|
-
try {
|
|
2672
|
-
await this.getStatus();
|
|
2673
|
-
return true;
|
|
2674
|
-
} catch {
|
|
2675
|
-
return false;
|
|
2676
|
-
}
|
|
2677
|
-
}
|
|
2678
|
-
};
|
|
2679
|
-
|
|
2680
|
-
// src/transports/cloud.ts
|
|
2681
|
-
var DEFAULT_BASE_URL = "https://api.adhf.dev";
|
|
2682
|
-
var CloudTransport = class {
|
|
2683
|
-
baseUrl;
|
|
2684
|
-
apiKey;
|
|
2685
|
-
constructor(opts) {
|
|
2686
|
-
this.apiKey = opts.apiKey;
|
|
2687
|
-
this.baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
2688
|
-
}
|
|
2689
|
-
headers() {
|
|
2690
|
-
return {
|
|
2691
|
-
"Content-Type": "application/json",
|
|
2692
|
-
"Authorization": `Bearer ${this.apiKey}`
|
|
2693
|
-
};
|
|
2694
|
-
}
|
|
2695
|
-
async listRemoteMeshes() {
|
|
2696
|
-
const res = await fetch(`${this.baseUrl}/api/v1/repo-meshes`, { headers: this.headers() });
|
|
2697
|
-
if (!res.ok) throw new Error(`List remote meshes failed: ${res.status}`);
|
|
3694
|
+
if (!res.ok) throw new Error(`Status fetch failed: ${res.status}`);
|
|
2698
3695
|
return res.json();
|
|
2699
3696
|
}
|
|
2700
|
-
async
|
|
2701
|
-
const res = await fetch(`${this.baseUrl}/api/v1/
|
|
3697
|
+
async command(type, args = {}) {
|
|
3698
|
+
const res = await fetch(`${this.baseUrl}/api/v1/command`, {
|
|
2702
3699
|
method: "POST",
|
|
2703
3700
|
headers: this.headers(),
|
|
2704
|
-
body: JSON.stringify(
|
|
2705
|
-
});
|
|
2706
|
-
if (!res.ok) throw new Error(`Create remote mesh failed: ${res.status}`);
|
|
2707
|
-
return res.json();
|
|
2708
|
-
}
|
|
2709
|
-
async deleteRemoteMesh(meshId) {
|
|
2710
|
-
const res = await fetch(`${this.baseUrl}/api/v1/repo-meshes/${encodeURIComponent(meshId)}`, {
|
|
2711
|
-
method: "DELETE",
|
|
2712
|
-
headers: this.headers()
|
|
3701
|
+
body: JSON.stringify({ type, ...args })
|
|
2713
3702
|
});
|
|
2714
|
-
if (!res.ok)
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
if (!res.ok) throw new Error(`List daemons failed: ${res.status}`);
|
|
2719
|
-
return res.json();
|
|
2720
|
-
}
|
|
2721
|
-
async getStatus(targetId) {
|
|
2722
|
-
const res = await fetch(
|
|
2723
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/status`,
|
|
2724
|
-
{ headers: this.headers() }
|
|
2725
|
-
);
|
|
2726
|
-
if (!res.ok) throw new Error(`Status failed: ${res.status}`);
|
|
2727
|
-
return res.json();
|
|
2728
|
-
}
|
|
2729
|
-
/** Get all sessions for a daemon (returns CompactSessionEntry[]). */
|
|
2730
|
-
async getDaemonStatus(daemonId) {
|
|
2731
|
-
const res = await fetch(
|
|
2732
|
-
`${this.baseUrl}/api/v1/daemons/${encodeURIComponent(daemonId)}/status`,
|
|
2733
|
-
{ headers: this.headers() }
|
|
2734
|
-
);
|
|
2735
|
-
if (!res.ok) throw new Error(`Daemon status failed: ${res.status}`);
|
|
2736
|
-
return res.json();
|
|
2737
|
-
}
|
|
2738
|
-
async readChat(targetId, opts = {}) {
|
|
2739
|
-
const params = new URLSearchParams();
|
|
2740
|
-
if (opts.limit) params.set("limit", String(opts.limit));
|
|
2741
|
-
if (opts.sessionId) params.set("sessionId", opts.sessionId);
|
|
2742
|
-
const qs = params.toString() ? `?${params}` : "";
|
|
2743
|
-
const res = await fetch(
|
|
2744
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat${qs}`,
|
|
2745
|
-
{ headers: this.headers() }
|
|
2746
|
-
);
|
|
2747
|
-
if (!res.ok) throw new Error(`Read chat failed: ${res.status}`);
|
|
2748
|
-
return res.json();
|
|
2749
|
-
}
|
|
2750
|
-
async getChatDebugBundle(targetId, opts = {}) {
|
|
2751
|
-
const res = await fetch(
|
|
2752
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat/debug`,
|
|
2753
|
-
{
|
|
2754
|
-
method: "POST",
|
|
2755
|
-
headers: this.headers(),
|
|
2756
|
-
body: JSON.stringify({
|
|
2757
|
-
...opts.agentType ? { agentType: opts.agentType } : {},
|
|
2758
|
-
...opts.sessionId ? { sessionId: opts.sessionId } : {},
|
|
2759
|
-
...opts.tailLimit ? { tailLimit: opts.tailLimit } : {},
|
|
2760
|
-
...opts.delivery ? { delivery: opts.delivery } : {}
|
|
2761
|
-
})
|
|
2762
|
-
}
|
|
2763
|
-
);
|
|
2764
|
-
if (!res.ok) throw new Error(`Chat debug bundle failed: ${res.status}`);
|
|
2765
|
-
return res.json();
|
|
2766
|
-
}
|
|
2767
|
-
async sendChat(targetId, message, opts = {}) {
|
|
2768
|
-
const res = await fetch(
|
|
2769
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/chat`,
|
|
2770
|
-
{
|
|
2771
|
-
method: "POST",
|
|
2772
|
-
headers: this.headers(),
|
|
2773
|
-
body: JSON.stringify({ message, ...opts })
|
|
2774
|
-
}
|
|
2775
|
-
);
|
|
2776
|
-
if (!res.ok) throw new Error(`Send chat failed: ${res.status}`);
|
|
2777
|
-
return res.json();
|
|
2778
|
-
}
|
|
2779
|
-
async approve(targetId, action, agentType) {
|
|
2780
|
-
const res = await fetch(
|
|
2781
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(targetId)}/approve`,
|
|
2782
|
-
{
|
|
2783
|
-
method: "POST",
|
|
2784
|
-
headers: this.headers(),
|
|
2785
|
-
body: JSON.stringify({ action, ...agentType ? { agentType } : {} })
|
|
2786
|
-
}
|
|
2787
|
-
);
|
|
2788
|
-
if (!res.ok) throw new Error(`Approve failed: ${res.status}`);
|
|
2789
|
-
return res.json();
|
|
2790
|
-
}
|
|
2791
|
-
async gitStatus(daemonId, workspace, includeDiff = true, refreshUpstream = false) {
|
|
2792
|
-
const params = new URLSearchParams({ workspace, includeDiff: String(includeDiff), refreshUpstream: String(refreshUpstream) });
|
|
2793
|
-
const res = await fetch(
|
|
2794
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-status?${params}`,
|
|
2795
|
-
{ headers: this.headers() }
|
|
2796
|
-
);
|
|
2797
|
-
if (!res.ok) throw new Error(`Git status failed: ${res.status}`);
|
|
2798
|
-
return res.json();
|
|
2799
|
-
}
|
|
2800
|
-
async stop(daemonId, opts) {
|
|
2801
|
-
const res = await fetch(
|
|
2802
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/stop`,
|
|
2803
|
-
{
|
|
2804
|
-
method: "POST",
|
|
2805
|
-
headers: this.headers(),
|
|
2806
|
-
body: JSON.stringify(opts)
|
|
2807
|
-
}
|
|
2808
|
-
);
|
|
2809
|
-
if (!res.ok) throw new Error(`Stop failed: ${res.status}`);
|
|
2810
|
-
return res.json();
|
|
2811
|
-
}
|
|
2812
|
-
async launch(daemonId, opts) {
|
|
2813
|
-
const res = await fetch(
|
|
2814
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/launch`,
|
|
2815
|
-
{
|
|
2816
|
-
method: "POST",
|
|
2817
|
-
headers: this.headers(),
|
|
2818
|
-
body: JSON.stringify(opts)
|
|
2819
|
-
}
|
|
2820
|
-
);
|
|
2821
|
-
if (!res.ok) throw new Error(`Launch failed: ${res.status}`);
|
|
2822
|
-
return res.json();
|
|
2823
|
-
}
|
|
2824
|
-
async gitLog(daemonId, workspace, opts = {}) {
|
|
2825
|
-
const params = new URLSearchParams({ workspace });
|
|
2826
|
-
if (opts.limit) params.set("limit", String(opts.limit));
|
|
2827
|
-
if (opts.file) params.set("file", opts.file);
|
|
2828
|
-
if (opts.since) params.set("since", opts.since);
|
|
2829
|
-
if (opts.until) params.set("until", opts.until);
|
|
2830
|
-
const res = await fetch(
|
|
2831
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-log?${params}`,
|
|
2832
|
-
{ headers: this.headers() }
|
|
2833
|
-
);
|
|
2834
|
-
if (!res.ok) throw new Error(`Git log failed: ${res.status}`);
|
|
2835
|
-
return res.json();
|
|
2836
|
-
}
|
|
2837
|
-
async gitDiff(daemonId, workspace, opts = {}) {
|
|
2838
|
-
const params = new URLSearchParams({ workspace });
|
|
2839
|
-
if (opts.file) params.set("file", opts.file);
|
|
2840
|
-
if (opts.maxLines) params.set("maxLines", String(opts.maxLines));
|
|
2841
|
-
if (opts.staged) params.set("staged", "true");
|
|
2842
|
-
const res = await fetch(
|
|
2843
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-diff?${params}`,
|
|
2844
|
-
{ headers: this.headers() }
|
|
2845
|
-
);
|
|
2846
|
-
if (!res.ok) throw new Error(`Git diff failed: ${res.status}`);
|
|
2847
|
-
return res.json();
|
|
2848
|
-
}
|
|
2849
|
-
async gitPush(daemonId, opts) {
|
|
2850
|
-
const res = await fetch(
|
|
2851
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-push`,
|
|
2852
|
-
{
|
|
2853
|
-
method: "POST",
|
|
2854
|
-
headers: this.headers(),
|
|
2855
|
-
body: JSON.stringify(opts)
|
|
2856
|
-
}
|
|
2857
|
-
);
|
|
2858
|
-
if (!res.ok) throw new Error(`Git push failed: ${res.status}`);
|
|
2859
|
-
return res.json();
|
|
2860
|
-
}
|
|
2861
|
-
async gitCheckpoint(daemonId, opts) {
|
|
2862
|
-
const res = await fetch(
|
|
2863
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/git-checkpoint`,
|
|
2864
|
-
{
|
|
2865
|
-
method: "POST",
|
|
2866
|
-
headers: this.headers(),
|
|
2867
|
-
body: JSON.stringify(opts)
|
|
2868
|
-
}
|
|
2869
|
-
);
|
|
2870
|
-
if (!res.ok) throw new Error(`Git checkpoint failed: ${res.status}`);
|
|
2871
|
-
return res.json();
|
|
2872
|
-
}
|
|
2873
|
-
async meshCloneNode(daemonId, payload) {
|
|
2874
|
-
const res = await fetch(
|
|
2875
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/mesh/clone-node`,
|
|
2876
|
-
{
|
|
2877
|
-
method: "POST",
|
|
2878
|
-
headers: this.headers(),
|
|
2879
|
-
body: JSON.stringify(payload)
|
|
2880
|
-
}
|
|
2881
|
-
);
|
|
2882
|
-
if (!res.ok) throw new Error(`Mesh clone node failed: ${res.status}`);
|
|
2883
|
-
return res.json();
|
|
2884
|
-
}
|
|
2885
|
-
async meshRemoveNode(daemonId, payload) {
|
|
2886
|
-
const res = await fetch(
|
|
2887
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/mesh/remove-node`,
|
|
2888
|
-
{
|
|
2889
|
-
method: "POST",
|
|
2890
|
-
headers: this.headers(),
|
|
2891
|
-
body: JSON.stringify(payload)
|
|
2892
|
-
}
|
|
2893
|
-
);
|
|
2894
|
-
if (!res.ok) throw new Error(`Mesh remove node failed: ${res.status}`);
|
|
2895
|
-
return res.json();
|
|
2896
|
-
}
|
|
2897
|
-
async meshCleanupSessions(daemonId, payload) {
|
|
2898
|
-
const res = await fetch(
|
|
2899
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/mesh/cleanup-sessions`,
|
|
2900
|
-
{
|
|
2901
|
-
method: "POST",
|
|
2902
|
-
headers: this.headers(),
|
|
2903
|
-
body: JSON.stringify(payload)
|
|
2904
|
-
}
|
|
2905
|
-
);
|
|
2906
|
-
if (!res.ok) throw new Error(`Mesh cleanup sessions failed: ${res.status}`);
|
|
2907
|
-
return res.json();
|
|
2908
|
-
}
|
|
2909
|
-
async meshEnqueueTask(daemonId, payload) {
|
|
2910
|
-
const res = await fetch(
|
|
2911
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/mesh/enqueue`,
|
|
2912
|
-
{
|
|
2913
|
-
method: "POST",
|
|
2914
|
-
headers: this.headers(),
|
|
2915
|
-
body: JSON.stringify(payload)
|
|
2916
|
-
}
|
|
2917
|
-
);
|
|
2918
|
-
if (!res.ok) throw new Error(`Mesh enqueue task failed: ${res.status}`);
|
|
2919
|
-
return res.json();
|
|
2920
|
-
}
|
|
2921
|
-
async meshRefineNode(daemonId, payload) {
|
|
2922
|
-
const res = await fetch(
|
|
2923
|
-
`${this.baseUrl}/api/v1/shortcuts/${encodeURIComponent(daemonId)}/mesh/refine-node`,
|
|
2924
|
-
{
|
|
2925
|
-
method: "POST",
|
|
2926
|
-
headers: this.headers(),
|
|
2927
|
-
body: JSON.stringify(payload)
|
|
2928
|
-
}
|
|
2929
|
-
);
|
|
2930
|
-
if (!res.ok) throw new Error(`Mesh refine node failed: ${res.status}`);
|
|
3703
|
+
if (!res.ok) {
|
|
3704
|
+
const text = await res.text().catch(() => res.statusText);
|
|
3705
|
+
throw new Error(`Command ${type} failed: ${res.status} ${text}`);
|
|
3706
|
+
}
|
|
2931
3707
|
return res.json();
|
|
2932
3708
|
}
|
|
2933
3709
|
async ping() {
|
|
2934
3710
|
try {
|
|
2935
|
-
await this.
|
|
3711
|
+
await this.getStatus();
|
|
2936
3712
|
return true;
|
|
2937
3713
|
} catch {
|
|
2938
3714
|
return false;
|
|
@@ -2950,14 +3726,10 @@ var FORMAT_PROP = {
|
|
|
2950
3726
|
};
|
|
2951
3727
|
var LIST_SESSIONS_TOOL = {
|
|
2952
3728
|
name: "list_sessions",
|
|
2953
|
-
description: "List all connected agent sessions.
|
|
3729
|
+
description: "List all connected agent sessions.",
|
|
2954
3730
|
inputSchema: {
|
|
2955
3731
|
type: "object",
|
|
2956
3732
|
properties: {
|
|
2957
|
-
daemon_id: {
|
|
2958
|
-
type: "string",
|
|
2959
|
-
description: "Daemon ID (cloud mode only). Omit to list sessions across all daemons."
|
|
2960
|
-
},
|
|
2961
3733
|
...FORMAT_PROP
|
|
2962
3734
|
},
|
|
2963
3735
|
required: []
|
|
@@ -2965,87 +3737,35 @@ var LIST_SESSIONS_TOOL = {
|
|
|
2965
3737
|
};
|
|
2966
3738
|
async function listSessions(transport, args = {}) {
|
|
2967
3739
|
const asJson = args.format === "json";
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
const sessions = status?.sessions ?? [];
|
|
2971
|
-
if (asJson) {
|
|
2972
|
-
return JSON.stringify({
|
|
2973
|
-
sessions: sessions.map((s) => ({
|
|
2974
|
-
id: s.id,
|
|
2975
|
-
type: s.providerType ?? s.type ?? "unknown",
|
|
2976
|
-
label: s.label ?? null,
|
|
2977
|
-
status: s.status ?? s.agentStatus ?? null,
|
|
2978
|
-
workspace: s.workspace ?? null
|
|
2979
|
-
}))
|
|
2980
|
-
}, null, 2);
|
|
2981
|
-
}
|
|
2982
|
-
if (sessions.length === 0) return "No active sessions.";
|
|
2983
|
-
const lines = sessions.map((s) => {
|
|
2984
|
-
const parts = [`id: ${s.id}`, `type: ${s.providerType ?? s.type ?? "unknown"}`];
|
|
2985
|
-
if (s.label) parts.push(`label: ${s.label}`);
|
|
2986
|
-
if (s.status ?? s.agentStatus) parts.push(`status: ${s.status ?? s.agentStatus}`);
|
|
2987
|
-
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
2988
|
-
return parts.join(", ");
|
|
2989
|
-
});
|
|
2990
|
-
return `Sessions (${sessions.length}):
|
|
2991
|
-
${lines.join("\n")}`;
|
|
2992
|
-
}
|
|
2993
|
-
return listSessionsCloud(transport, args.daemon_id, asJson);
|
|
2994
|
-
}
|
|
2995
|
-
async function listSessionsCloud(transport, daemonId, asJson) {
|
|
2996
|
-
const collected = [];
|
|
2997
|
-
if (daemonId) {
|
|
2998
|
-
const daemonStatus = await transport.getDaemonStatus(daemonId);
|
|
2999
|
-
for (const s of daemonStatus?.sessions ?? []) {
|
|
3000
|
-
collected.push({ daemonId, session: s });
|
|
3001
|
-
}
|
|
3002
|
-
} else {
|
|
3003
|
-
const data = await transport.listDaemons();
|
|
3004
|
-
const daemons = data?.daemons ?? [];
|
|
3005
|
-
for (let i = 0; i < daemons.length; i += 5) {
|
|
3006
|
-
await Promise.allSettled(
|
|
3007
|
-
daemons.slice(i, i + 5).map(async (d) => {
|
|
3008
|
-
try {
|
|
3009
|
-
const daemonStatus = await transport.getDaemonStatus(d.id);
|
|
3010
|
-
for (const s of daemonStatus?.sessions ?? []) {
|
|
3011
|
-
collected.push({ daemonId: d.id, session: s });
|
|
3012
|
-
}
|
|
3013
|
-
} catch {
|
|
3014
|
-
}
|
|
3015
|
-
})
|
|
3016
|
-
);
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3740
|
+
const status = await transport.getStatus();
|
|
3741
|
+
const sessions = status?.sessions ?? [];
|
|
3019
3742
|
if (asJson) {
|
|
3020
3743
|
return JSON.stringify({
|
|
3021
|
-
sessions:
|
|
3022
|
-
daemon_id: dId,
|
|
3744
|
+
sessions: sessions.map((s) => ({
|
|
3023
3745
|
id: s.id,
|
|
3024
|
-
type: s.providerType ?? "unknown",
|
|
3025
|
-
|
|
3746
|
+
type: s.providerType ?? s.type ?? "unknown",
|
|
3747
|
+
label: s.label ?? null,
|
|
3748
|
+
status: s.status ?? s.agentStatus ?? null,
|
|
3026
3749
|
workspace: s.workspace ?? null
|
|
3027
3750
|
}))
|
|
3028
3751
|
}, null, 2);
|
|
3029
3752
|
}
|
|
3030
|
-
if (
|
|
3031
|
-
const lines =
|
|
3032
|
-
const parts = [
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
`type: ${s.providerType ?? "unknown"}`
|
|
3036
|
-
];
|
|
3037
|
-
if (s.status) parts.push(`status: ${s.status}`);
|
|
3753
|
+
if (sessions.length === 0) return "No active sessions.";
|
|
3754
|
+
const lines = sessions.map((s) => {
|
|
3755
|
+
const parts = [`id: ${s.id}`, `type: ${s.providerType ?? s.type ?? "unknown"}`];
|
|
3756
|
+
if (s.label) parts.push(`label: ${s.label}`);
|
|
3757
|
+
if (s.status ?? s.agentStatus) parts.push(`status: ${s.status ?? s.agentStatus}`);
|
|
3038
3758
|
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
3039
3759
|
return parts.join(", ");
|
|
3040
3760
|
});
|
|
3041
|
-
return `Sessions (${
|
|
3761
|
+
return `Sessions (${sessions.length}):
|
|
3042
3762
|
${lines.join("\n")}`;
|
|
3043
3763
|
}
|
|
3044
3764
|
|
|
3045
3765
|
// src/tools/list-daemons.ts
|
|
3046
3766
|
var LIST_DAEMONS_TOOL = {
|
|
3047
3767
|
name: "list_daemons",
|
|
3048
|
-
description: "List
|
|
3768
|
+
description: "List the connected daemon (machine running the ADHDev agent). Returns the daemon identity extracted from its status report.",
|
|
3049
3769
|
inputSchema: {
|
|
3050
3770
|
type: "object",
|
|
3051
3771
|
properties: {
|
|
@@ -3056,46 +3776,17 @@ var LIST_DAEMONS_TOOL = {
|
|
|
3056
3776
|
};
|
|
3057
3777
|
async function listDaemons(transport, args = {}) {
|
|
3058
3778
|
const asJson = args.format === "json";
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
return `Daemons (1):
|
|
3779
|
+
const status = await transport.getStatus();
|
|
3780
|
+
const daemon = {
|
|
3781
|
+
id: status?.id ?? status?.instanceId ?? "standalone",
|
|
3782
|
+
hostname: status?.hostname ?? status?.machine?.hostname ?? "localhost",
|
|
3783
|
+
platform: status?.platform ?? status?.machine?.platform ?? "unknown",
|
|
3784
|
+
version: status?.version ?? null,
|
|
3785
|
+
sessions: (status?.sessions ?? []).length
|
|
3786
|
+
};
|
|
3787
|
+
if (asJson) return JSON.stringify({ daemons: [daemon] }, null, 2);
|
|
3788
|
+
return `Daemons (1):
|
|
3070
3789
|
id: ${daemon.id}, hostname: ${daemon.hostname}, platform: ${daemon.platform}${daemon.version ? `, version: ${daemon.version}` : ""}, sessions: ${daemon.sessions}`;
|
|
3071
|
-
}
|
|
3072
|
-
const data = await transport.listDaemons();
|
|
3073
|
-
const daemons = data?.daemons ?? [];
|
|
3074
|
-
if (asJson) {
|
|
3075
|
-
return JSON.stringify({
|
|
3076
|
-
daemons: daemons.map((d) => ({
|
|
3077
|
-
id: d.id,
|
|
3078
|
-
hostname: d.hostname ?? null,
|
|
3079
|
-
platform: d.platform ?? null,
|
|
3080
|
-
nickname: d.nickname ?? null,
|
|
3081
|
-
version: d.version ?? null,
|
|
3082
|
-
p2p_available: d.p2p?.available ?? null,
|
|
3083
|
-
cdp_connected: d.cdpConnected ?? null
|
|
3084
|
-
}))
|
|
3085
|
-
}, null, 2);
|
|
3086
|
-
}
|
|
3087
|
-
if (daemons.length === 0) return "No connected daemons.";
|
|
3088
|
-
const lines = daemons.map((d) => {
|
|
3089
|
-
const parts = [`id: ${d.id}`];
|
|
3090
|
-
if (d.nickname) parts.push(`nickname: ${d.nickname}`);
|
|
3091
|
-
if (d.hostname) parts.push(`hostname: ${d.hostname}`);
|
|
3092
|
-
if (d.platform) parts.push(`platform: ${d.platform}`);
|
|
3093
|
-
if (d.version) parts.push(`version: ${d.version}`);
|
|
3094
|
-
if (d.p2p?.available != null) parts.push(`p2p: ${d.p2p.available ? "yes" : "no"}`);
|
|
3095
|
-
return parts.join(", ");
|
|
3096
|
-
});
|
|
3097
|
-
return `Daemons (${daemons.length}):
|
|
3098
|
-
${lines.join("\n")}`;
|
|
3099
3790
|
}
|
|
3100
3791
|
|
|
3101
3792
|
// src/tools/read-chat.ts
|
|
@@ -3113,10 +3804,6 @@ var READ_CHAT_TOOL = {
|
|
|
3113
3804
|
type: "number",
|
|
3114
3805
|
description: "Max messages to return (default: 50)."
|
|
3115
3806
|
},
|
|
3116
|
-
daemon_id: {
|
|
3117
|
-
type: "string",
|
|
3118
|
-
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
3119
|
-
},
|
|
3120
3807
|
compact: {
|
|
3121
3808
|
type: "boolean",
|
|
3122
3809
|
description: "Opt-in compact mode: filters tool/terminal/system/internal/control/debug/status chatter and returns user-visible messages plus lightweight summary metadata."
|
|
@@ -3128,23 +3815,12 @@ var READ_CHAT_TOOL = {
|
|
|
3128
3815
|
};
|
|
3129
3816
|
async function readChat(transport, args) {
|
|
3130
3817
|
const limit = args.limit ?? 50;
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
});
|
|
3136
|
-
const annotated2 = annotateRapidReadChatAdvisory(result2, {
|
|
3137
|
-
key: `local:${args.session_id ?? "__active__"}`,
|
|
3138
|
-
toolName: "read_chat",
|
|
3139
|
-
completionCallbackExpected: false
|
|
3140
|
-
});
|
|
3141
|
-
return formatChatResult(annotated2, args.session_id, args.format, limit, args.compact);
|
|
3142
|
-
}
|
|
3143
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3144
|
-
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
3145
|
-
const result = await transport.readChat(targetId, { limit, sessionId: args.session_id });
|
|
3818
|
+
const result = await transport.command("read_chat", {
|
|
3819
|
+
...args.session_id ? { targetSessionId: args.session_id } : {},
|
|
3820
|
+
tailLimit: limit
|
|
3821
|
+
});
|
|
3146
3822
|
const annotated = annotateRapidReadChatAdvisory(result, {
|
|
3147
|
-
key: `
|
|
3823
|
+
key: `local:${args.session_id ?? "__active__"}`,
|
|
3148
3824
|
toolName: "read_chat",
|
|
3149
3825
|
completionCallbackExpected: false
|
|
3150
3826
|
});
|
|
@@ -3185,11 +3861,17 @@ function formatChatResult(result, sessionId, format, limit = 50, compact = false
|
|
|
3185
3861
|
}, null, 2);
|
|
3186
3862
|
}
|
|
3187
3863
|
if ((format === "text" || format === void 0) && compact && compactPayload) {
|
|
3188
|
-
const
|
|
3864
|
+
const summaryText = typeof compactPayload.summary === "string" ? compactPayload.summary.trim() : "";
|
|
3865
|
+
const tail = outputMessages.slice(-limit);
|
|
3866
|
+
const lastIndex = tail.length - 1;
|
|
3867
|
+
const lines2 = tail.flatMap((m, idx) => {
|
|
3189
3868
|
const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
|
|
3190
3869
|
const content = messageContent(m);
|
|
3870
|
+
if (idx === lastIndex && (role === "Agent" || m.role === "agent") && summaryText && content.trim() === summaryText) {
|
|
3871
|
+
return [];
|
|
3872
|
+
}
|
|
3191
3873
|
const truncated = content.length > 500 ? `${content.slice(0, 500)}\u2026` : content;
|
|
3192
|
-
return `[${role}] ${truncated}
|
|
3874
|
+
return [`[${role}] ${truncated}`];
|
|
3193
3875
|
});
|
|
3194
3876
|
if (compactPayload.summary) {
|
|
3195
3877
|
const truncatedSummary = compactPayload.summary.length > 500 ? `${compactPayload.summary.slice(0, 500)}\u2026` : compactPayload.summary;
|
|
@@ -3228,10 +3910,6 @@ var READ_CHAT_DEBUG_TOOL = {
|
|
|
3228
3910
|
type: "string",
|
|
3229
3911
|
description: "Target session ID (from list_sessions). Required for reliable routing."
|
|
3230
3912
|
},
|
|
3231
|
-
daemon_id: {
|
|
3232
|
-
type: "string",
|
|
3233
|
-
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
3234
|
-
},
|
|
3235
3913
|
agent_type: {
|
|
3236
3914
|
type: "string",
|
|
3237
3915
|
description: "Optional provider/agent type hint, e.g. hermes-cli, claude-cli, codex-cli."
|
|
@@ -3261,19 +3939,7 @@ async function readChatDebug(transport, args) {
|
|
|
3261
3939
|
...args.agent_type ? { agentType: args.agent_type, providerType: args.agent_type } : {},
|
|
3262
3940
|
...delivery === "daemon_file" ? { delivery: "daemon_file" } : {}
|
|
3263
3941
|
};
|
|
3264
|
-
|
|
3265
|
-
if (isLocalTransport(transport)) {
|
|
3266
|
-
result = await transport.command("get_chat_debug_bundle", commandArgs);
|
|
3267
|
-
} else {
|
|
3268
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3269
|
-
const targetId = `${args.daemon_id}:session:${sessionId}`;
|
|
3270
|
-
result = await transport.getChatDebugBundle(targetId, {
|
|
3271
|
-
sessionId,
|
|
3272
|
-
agentType: args.agent_type,
|
|
3273
|
-
tailLimit,
|
|
3274
|
-
delivery
|
|
3275
|
-
});
|
|
3276
|
-
}
|
|
3942
|
+
const result = await transport.command("get_chat_debug_bundle", commandArgs);
|
|
3277
3943
|
return formatChatDebugResult(result, { sessionId, delivery, format: args.format });
|
|
3278
3944
|
}
|
|
3279
3945
|
function formatChatDebugResult(result, options) {
|
|
@@ -3304,6 +3970,79 @@ function formatChatDebugResult(result, options) {
|
|
|
3304
3970
|
return JSON.stringify(result, null, 2);
|
|
3305
3971
|
}
|
|
3306
3972
|
|
|
3973
|
+
// src/tools/spec-debug.ts
|
|
3974
|
+
var SPEC_DEBUG_TOOL = {
|
|
3975
|
+
name: "spec_debug",
|
|
3976
|
+
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.",
|
|
3977
|
+
inputSchema: {
|
|
3978
|
+
type: "object",
|
|
3979
|
+
properties: {
|
|
3980
|
+
session_id: {
|
|
3981
|
+
type: "string",
|
|
3982
|
+
description: "Target session ID (from list_sessions)."
|
|
3983
|
+
},
|
|
3984
|
+
...FORMAT_PROP
|
|
3985
|
+
},
|
|
3986
|
+
required: ["session_id"]
|
|
3987
|
+
}
|
|
3988
|
+
};
|
|
3989
|
+
async function specDebug(transport, args) {
|
|
3990
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id.trim() : "";
|
|
3991
|
+
if (!sessionId) throw new Error("session_id is required");
|
|
3992
|
+
const result = await transport.command("get_spec_debug", { targetSessionId: sessionId });
|
|
3993
|
+
return formatSpecDebugResult(result, { sessionId, format: args.format });
|
|
3994
|
+
}
|
|
3995
|
+
function formatSpecDebugResult(result, options) {
|
|
3996
|
+
if (!result?.success) {
|
|
3997
|
+
const err = result?.error || "Unknown error";
|
|
3998
|
+
if (options.format === "json") return JSON.stringify({ success: false, error: err }, null, 2);
|
|
3999
|
+
return `Error: ${err}`;
|
|
4000
|
+
}
|
|
4001
|
+
if (options.format === "json") return JSON.stringify(result, null, 2);
|
|
4002
|
+
const snap = result.snapshot;
|
|
4003
|
+
if (!snap) {
|
|
4004
|
+
return [
|
|
4005
|
+
`session_id: ${options.sessionId}`,
|
|
4006
|
+
`provider_type: ${String(result.providerType || "")}`,
|
|
4007
|
+
"is_spec_provider: false",
|
|
4008
|
+
"No spec debug data available (not a spec-driven provider)."
|
|
4009
|
+
].join("\n");
|
|
4010
|
+
}
|
|
4011
|
+
const lines = [];
|
|
4012
|
+
lines.push(`session_id: ${options.sessionId}`);
|
|
4013
|
+
lines.push(`provider_type: ${String(result.providerType || snap.cliType || "")}`);
|
|
4014
|
+
lines.push(`spec_id: ${String(snap.spec_id || "")}`);
|
|
4015
|
+
lines.push(`spec_path: ${String(snap.specPath || "")}`);
|
|
4016
|
+
lines.push(`current_state: ${snap.current_state ? `${snap.current_state.id} (${snap.current_state.label})` : "none"}`);
|
|
4017
|
+
lines.push(`idle_hold_pending: ${String(snap.idleHoldPending ?? false)}`);
|
|
4018
|
+
lines.push(`last_busy_at: ${snap.lastBusyAt ? new Date(snap.lastBusyAt).toISOString() : "never"}`);
|
|
4019
|
+
lines.push(`exited: ${String(snap.exited ?? false)}`);
|
|
4020
|
+
if (snap.current_modal) {
|
|
4021
|
+
lines.push(`current_modal: ${JSON.stringify(snap.current_modal)}`);
|
|
4022
|
+
}
|
|
4023
|
+
if (snap.sections && typeof snap.sections === "object") {
|
|
4024
|
+
lines.push("");
|
|
4025
|
+
lines.push("\u2500\u2500 sections \u2500\u2500");
|
|
4026
|
+
for (const [id, text] of Object.entries(snap.sections)) {
|
|
4027
|
+
const preview = String(text || "").replace(/\n/g, "\u21B5").slice(0, 120);
|
|
4028
|
+
lines.push(` ${id}: ${preview}`);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
const history = Array.isArray(snap.stateHistory) ? snap.stateHistory : [];
|
|
4032
|
+
if (history.length > 0) {
|
|
4033
|
+
lines.push("");
|
|
4034
|
+
lines.push("\u2500\u2500 state history (newest first) \u2500\u2500");
|
|
4035
|
+
const now = Date.now();
|
|
4036
|
+
for (const entry of [...history].reverse().slice(0, 20)) {
|
|
4037
|
+
const agoMs = now - entry.at;
|
|
4038
|
+
const ago = agoMs < 2e3 ? `${agoMs}ms ago` : `${(agoMs / 1e3).toFixed(1)}s ago`;
|
|
4039
|
+
const dur = entry.durationMs > 0 ? ` held ${entry.durationMs}ms` : "";
|
|
4040
|
+
lines.push(` ${String(entry.stateId).padEnd(18)} ${ago}${dur}`);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
return lines.join("\n");
|
|
4044
|
+
}
|
|
4045
|
+
|
|
3307
4046
|
// src/tools/send-chat.ts
|
|
3308
4047
|
var SEND_CHAT_TOOL = {
|
|
3309
4048
|
name: "send_chat",
|
|
@@ -3318,10 +4057,6 @@ var SEND_CHAT_TOOL = {
|
|
|
3318
4057
|
session_id: {
|
|
3319
4058
|
type: "string",
|
|
3320
4059
|
description: "Target session ID (from list_sessions). Omit to use the active session."
|
|
3321
|
-
},
|
|
3322
|
-
daemon_id: {
|
|
3323
|
-
type: "string",
|
|
3324
|
-
description: "Daemon ID (cloud mode only). Omit for local mode."
|
|
3325
4060
|
}
|
|
3326
4061
|
},
|
|
3327
4062
|
required: ["message"]
|
|
@@ -3329,18 +4064,9 @@ var SEND_CHAT_TOOL = {
|
|
|
3329
4064
|
};
|
|
3330
4065
|
async function sendChat(transport, args) {
|
|
3331
4066
|
if (!args.message?.trim()) throw new Error("message is required");
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
3336
|
-
});
|
|
3337
|
-
if (result2?.success === false) return `Error: ${result2.error ?? "send_chat failed"}`;
|
|
3338
|
-
return "Message sent.";
|
|
3339
|
-
}
|
|
3340
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3341
|
-
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
3342
|
-
const result = await transport.sendChat(targetId, args.message, {
|
|
3343
|
-
...args.session_id ? { sessionId: args.session_id } : {}
|
|
4067
|
+
const result = await transport.command("send_chat", {
|
|
4068
|
+
message: args.message,
|
|
4069
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
3344
4070
|
});
|
|
3345
4071
|
if (result?.success === false) return `Error: ${result.error ?? "send_chat failed"}`;
|
|
3346
4072
|
return "Message sent.";
|
|
@@ -3361,10 +4087,6 @@ var APPROVE_TOOL = {
|
|
|
3361
4087
|
session_id: {
|
|
3362
4088
|
type: "string",
|
|
3363
4089
|
description: "Target session ID. Omit to use the active session."
|
|
3364
|
-
},
|
|
3365
|
-
daemon_id: {
|
|
3366
|
-
type: "string",
|
|
3367
|
-
description: "Daemon ID (cloud mode only)."
|
|
3368
4090
|
}
|
|
3369
4091
|
},
|
|
3370
4092
|
required: ["action"]
|
|
@@ -3372,25 +4094,18 @@ var APPROVE_TOOL = {
|
|
|
3372
4094
|
};
|
|
3373
4095
|
async function approve(transport, args) {
|
|
3374
4096
|
const action = args.action === "reject" ? "reject" : "approve";
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
if (result2?.success === false) return `Error: ${result2.error ?? "resolve_action failed"}`;
|
|
3381
|
-
return `Action ${action}d.`;
|
|
3382
|
-
}
|
|
3383
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3384
|
-
const targetId = args.session_id ? `${args.daemon_id}:session:${args.session_id}` : args.daemon_id;
|
|
3385
|
-
const result = await transport.approve(targetId, action);
|
|
3386
|
-
if (result?.success === false) return `Error: ${result.error ?? "approve failed"}`;
|
|
4097
|
+
const result = await transport.command("resolve_action", {
|
|
4098
|
+
action,
|
|
4099
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
4100
|
+
});
|
|
4101
|
+
if (result?.success === false) return `Error: ${result.error ?? "resolve_action failed"}`;
|
|
3387
4102
|
return `Action ${action}d.`;
|
|
3388
4103
|
}
|
|
3389
4104
|
|
|
3390
4105
|
// src/tools/screenshot.ts
|
|
3391
4106
|
var SCREENSHOT_TOOL = {
|
|
3392
4107
|
name: "screenshot",
|
|
3393
|
-
description: "Capture a screenshot of the current IDE window. Returns the image.
|
|
4108
|
+
description: "Capture a screenshot of the current IDE window. Returns the image.",
|
|
3394
4109
|
inputSchema: {
|
|
3395
4110
|
type: "object",
|
|
3396
4111
|
properties: {
|
|
@@ -3403,14 +4118,9 @@ var SCREENSHOT_TOOL = {
|
|
|
3403
4118
|
}
|
|
3404
4119
|
};
|
|
3405
4120
|
async function screenshot(transport, args) {
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
3410
|
-
});
|
|
3411
|
-
} else {
|
|
3412
|
-
return { type: "text", text: "Screenshots are not available in cloud mode. Run adhdev mcp in local mode (requires standalone daemon)." };
|
|
3413
|
-
}
|
|
4121
|
+
const result = await transport.command("screenshot", {
|
|
4122
|
+
...args.session_id ? { targetSessionId: args.session_id } : {}
|
|
4123
|
+
});
|
|
3414
4124
|
if (result?.success === false) {
|
|
3415
4125
|
return { type: "text", text: `Error: ${result.error ?? "screenshot failed"}` };
|
|
3416
4126
|
}
|
|
@@ -3437,42 +4147,22 @@ var GIT_STATUS_TOOL = {
|
|
|
3437
4147
|
type: "boolean",
|
|
3438
4148
|
description: "Include changed file list (default: true)."
|
|
3439
4149
|
},
|
|
3440
|
-
daemon_id: {
|
|
3441
|
-
type: "string",
|
|
3442
|
-
description: "Daemon ID (cloud mode only)."
|
|
3443
|
-
},
|
|
3444
4150
|
...FORMAT_PROP
|
|
3445
4151
|
},
|
|
3446
4152
|
required: ["workspace"]
|
|
3447
4153
|
}
|
|
3448
4154
|
};
|
|
3449
4155
|
async function gitStatus(transport, args) {
|
|
3450
|
-
let status;
|
|
3451
4156
|
let diffSummary;
|
|
3452
|
-
|
|
3453
|
-
|
|
4157
|
+
const statusResult = await transport.command("git_status", {
|
|
4158
|
+
workspace: args.workspace
|
|
4159
|
+
});
|
|
4160
|
+
const status = statusResult?.status ?? statusResult;
|
|
4161
|
+
if (args.include_diff !== false) {
|
|
4162
|
+
const diffResult = await transport.command("git_diff_summary", {
|
|
3454
4163
|
workspace: args.workspace
|
|
3455
4164
|
});
|
|
3456
|
-
|
|
3457
|
-
if (args.include_diff !== false) {
|
|
3458
|
-
const diffResult = await transport.command("git_diff_summary", {
|
|
3459
|
-
workspace: args.workspace
|
|
3460
|
-
});
|
|
3461
|
-
diffSummary = diffResult?.diffSummary ?? diffResult;
|
|
3462
|
-
}
|
|
3463
|
-
} else {
|
|
3464
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3465
|
-
const result = await transport.gitStatus(
|
|
3466
|
-
args.daemon_id,
|
|
3467
|
-
args.workspace,
|
|
3468
|
-
args.include_diff !== false
|
|
3469
|
-
);
|
|
3470
|
-
if (result?.error) {
|
|
3471
|
-
if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
|
|
3472
|
-
return `Error: ${result.error}`;
|
|
3473
|
-
}
|
|
3474
|
-
status = result?.status;
|
|
3475
|
-
diffSummary = result?.diff;
|
|
4165
|
+
diffSummary = diffResult?.diffSummary ?? diffResult;
|
|
3476
4166
|
}
|
|
3477
4167
|
if (status?.success === false || status?.reason) {
|
|
3478
4168
|
const msg = status?.error ?? status?.reason ?? "unknown";
|
|
@@ -3564,10 +4254,6 @@ var GIT_LOG_TOOL = {
|
|
|
3564
4254
|
type: "string",
|
|
3565
4255
|
description: "Only commits before this date (ISO 8601 or git date string, optional)."
|
|
3566
4256
|
},
|
|
3567
|
-
daemon_id: {
|
|
3568
|
-
type: "string",
|
|
3569
|
-
description: "Daemon ID (cloud mode only, required)."
|
|
3570
|
-
},
|
|
3571
4257
|
...FORMAT_PROP
|
|
3572
4258
|
},
|
|
3573
4259
|
required: ["workspace"]
|
|
@@ -3575,26 +4261,14 @@ var GIT_LOG_TOOL = {
|
|
|
3575
4261
|
};
|
|
3576
4262
|
async function gitLog(transport, args) {
|
|
3577
4263
|
const limit = Math.max(1, Math.min(100, args.limit ?? 20));
|
|
3578
|
-
let raw
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
});
|
|
3587
|
-
raw = raw?.log ?? raw;
|
|
3588
|
-
} else {
|
|
3589
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3590
|
-
const result = await transport.gitLog(args.daemon_id, args.workspace, {
|
|
3591
|
-
limit,
|
|
3592
|
-
file: args.file,
|
|
3593
|
-
since: args.since,
|
|
3594
|
-
until: args.until
|
|
3595
|
-
});
|
|
3596
|
-
raw = result?.log ?? result;
|
|
3597
|
-
}
|
|
4264
|
+
let raw = await transport.command("git_log", {
|
|
4265
|
+
workspace: args.workspace,
|
|
4266
|
+
limit,
|
|
4267
|
+
...args.file ? { path: args.file } : {},
|
|
4268
|
+
...args.since ? { since: args.since } : {},
|
|
4269
|
+
...args.until ? { until: args.until } : {}
|
|
4270
|
+
});
|
|
4271
|
+
raw = raw?.log ?? raw;
|
|
3598
4272
|
if (raw?.success === false || raw?.reason) {
|
|
3599
4273
|
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
3600
4274
|
if (args.format === "json") return JSON.stringify({ error: msg }, null, 2);
|
|
@@ -3657,10 +4331,6 @@ var GIT_DIFF_TOOL = {
|
|
|
3657
4331
|
type: "boolean",
|
|
3658
4332
|
description: "Show staged changes instead of unstaged (default: false)."
|
|
3659
4333
|
},
|
|
3660
|
-
daemon_id: {
|
|
3661
|
-
type: "string",
|
|
3662
|
-
description: "Daemon ID (cloud mode only, required)."
|
|
3663
|
-
},
|
|
3664
4334
|
...FORMAT_PROP
|
|
3665
4335
|
},
|
|
3666
4336
|
required: ["workspace"]
|
|
@@ -3669,20 +4339,7 @@ var GIT_DIFF_TOOL = {
|
|
|
3669
4339
|
async function gitDiff(transport, args) {
|
|
3670
4340
|
const maxLines = Math.max(10, Math.min(2e3, args.max_lines ?? 300));
|
|
3671
4341
|
const staged = args.staged ?? false;
|
|
3672
|
-
|
|
3673
|
-
return localGitDiff(transport, args.workspace, args.file, maxLines, staged, args.format);
|
|
3674
|
-
}
|
|
3675
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3676
|
-
const result = await transport.gitDiff(args.daemon_id, args.workspace, {
|
|
3677
|
-
file: args.file,
|
|
3678
|
-
maxLines,
|
|
3679
|
-
staged
|
|
3680
|
-
});
|
|
3681
|
-
if (result?.error) {
|
|
3682
|
-
if (args.format === "json") return JSON.stringify({ error: result.error }, null, 2);
|
|
3683
|
-
return `Git diff error: ${result.error}`;
|
|
3684
|
-
}
|
|
3685
|
-
return formatDiffResult(result, args.format);
|
|
4342
|
+
return localGitDiff(transport, args.workspace, args.file, maxLines, staged, args.format);
|
|
3686
4343
|
}
|
|
3687
4344
|
async function localGitDiff(transport, workspace, file, maxLines, staged, format) {
|
|
3688
4345
|
if (file) {
|
|
@@ -3804,10 +4461,6 @@ var GIT_CHECKPOINT_TOOL = {
|
|
|
3804
4461
|
include_untracked: {
|
|
3805
4462
|
type: "boolean",
|
|
3806
4463
|
description: "Also stage and commit untracked files (default: false)."
|
|
3807
|
-
},
|
|
3808
|
-
daemon_id: {
|
|
3809
|
-
type: "string",
|
|
3810
|
-
description: "Daemon ID (cloud mode only, required)."
|
|
3811
4464
|
}
|
|
3812
4465
|
},
|
|
3813
4466
|
required: ["workspace", "message"]
|
|
@@ -3817,23 +4470,12 @@ async function gitCheckpoint(transport, args) {
|
|
|
3817
4470
|
const message = args.message?.trim();
|
|
3818
4471
|
if (!message) return "Error: message is required";
|
|
3819
4472
|
if (message.length > 200) return "Error: message must be 200 characters or fewer";
|
|
3820
|
-
let raw
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
});
|
|
3827
|
-
raw = raw?.checkpoint ?? raw;
|
|
3828
|
-
} else {
|
|
3829
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3830
|
-
const result = await transport.gitCheckpoint(args.daemon_id, {
|
|
3831
|
-
workspace: args.workspace,
|
|
3832
|
-
message,
|
|
3833
|
-
includeUntracked: args.include_untracked ?? false
|
|
3834
|
-
});
|
|
3835
|
-
raw = result?.checkpoint ?? result;
|
|
3836
|
-
}
|
|
4473
|
+
let raw = await transport.command("git_checkpoint", {
|
|
4474
|
+
workspace: args.workspace,
|
|
4475
|
+
message,
|
|
4476
|
+
includeUntracked: args.include_untracked ?? false
|
|
4477
|
+
});
|
|
4478
|
+
raw = raw?.checkpoint ?? raw;
|
|
3837
4479
|
if (raw?.success === false || raw?.reason) {
|
|
3838
4480
|
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
3839
4481
|
if (msg.includes("Nothing to commit") || msg.includes("nothing to commit")) {
|
|
@@ -3864,33 +4506,18 @@ var GIT_PUSH_TOOL = {
|
|
|
3864
4506
|
branch: {
|
|
3865
4507
|
type: "string",
|
|
3866
4508
|
description: "Branch to push (default: current branch)."
|
|
3867
|
-
},
|
|
3868
|
-
daemon_id: {
|
|
3869
|
-
type: "string",
|
|
3870
|
-
description: "Daemon ID (cloud mode only, required)."
|
|
3871
4509
|
}
|
|
3872
4510
|
},
|
|
3873
4511
|
required: ["workspace"]
|
|
3874
4512
|
}
|
|
3875
4513
|
};
|
|
3876
4514
|
async function gitPush(transport, args) {
|
|
3877
|
-
let raw
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
});
|
|
3884
|
-
raw = raw?.push ?? raw;
|
|
3885
|
-
} else {
|
|
3886
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3887
|
-
const result = await transport.gitPush(args.daemon_id, {
|
|
3888
|
-
workspace: args.workspace,
|
|
3889
|
-
remote: args.remote,
|
|
3890
|
-
branch: args.branch
|
|
3891
|
-
});
|
|
3892
|
-
raw = result?.push ?? result;
|
|
3893
|
-
}
|
|
4515
|
+
let raw = await transport.command("git_push", {
|
|
4516
|
+
workspace: args.workspace,
|
|
4517
|
+
remote: args.remote ?? "origin",
|
|
4518
|
+
...args.branch ? { branch: args.branch } : {}
|
|
4519
|
+
});
|
|
4520
|
+
raw = raw?.push ?? raw;
|
|
3894
4521
|
if (raw?.success === false || raw?.reason) {
|
|
3895
4522
|
const msg = raw?.error ?? raw?.reason ?? "unknown";
|
|
3896
4523
|
return `Git push error: ${msg}`;
|
|
@@ -3921,32 +4548,17 @@ var LAUNCH_SESSION_TOOL = {
|
|
|
3921
4548
|
model: {
|
|
3922
4549
|
type: "string",
|
|
3923
4550
|
description: "Model override for ACP agents (e.g. claude-opus-4-7)."
|
|
3924
|
-
},
|
|
3925
|
-
daemon_id: {
|
|
3926
|
-
type: "string",
|
|
3927
|
-
description: "Daemon ID (cloud mode only). Required in cloud mode."
|
|
3928
4551
|
}
|
|
3929
4552
|
},
|
|
3930
4553
|
required: ["type"]
|
|
3931
4554
|
}
|
|
3932
4555
|
};
|
|
3933
4556
|
async function launchSession(transport, args) {
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
if (result2?.success === false) return `Error: ${result2.error ?? "launch failed"}`;
|
|
3940
|
-
const id2 = result2?.id ?? result2?.sessionId;
|
|
3941
|
-
return id2 ? `Session launched. id: ${id2}, type: ${args.type}` : `Launched: ${JSON.stringify(result2)}`;
|
|
3942
|
-
}
|
|
3943
|
-
if (!args.daemon_id) throw new Error("daemon_id is required in cloud mode");
|
|
3944
|
-
const result = await transport.launch(args.daemon_id, {
|
|
3945
|
-
type: args.type,
|
|
3946
|
-
dir: args.workspace,
|
|
3947
|
-
model: args.model
|
|
3948
|
-
});
|
|
3949
|
-
if (result?.success === false || result?.error) return `Error: ${result.error ?? "launch failed"}`;
|
|
4557
|
+
const isCliOrAcp = args.type.includes("-cli") || args.type.includes("-acp") || args.type === "codex";
|
|
4558
|
+
const commandType = isCliOrAcp ? "launch_cli" : "launch_ide";
|
|
4559
|
+
const payload = isCliOrAcp ? { cliType: args.type, dir: args.workspace ?? "~", ...args.model ? { model: args.model } : {} } : { ideType: args.type, enableCdp: true };
|
|
4560
|
+
const result = await transport.command(commandType, payload);
|
|
4561
|
+
if (result?.success === false) return `Error: ${result.error ?? "launch failed"}`;
|
|
3950
4562
|
const id = result?.id ?? result?.sessionId;
|
|
3951
4563
|
return id ? `Session launched. id: ${id}, type: ${args.type}` : `Launched: ${JSON.stringify(result)}`;
|
|
3952
4564
|
}
|
|
@@ -3962,43 +4574,29 @@ var STOP_SESSION_TOOL = {
|
|
|
3962
4574
|
type: "string",
|
|
3963
4575
|
description: "Session ID to stop (from list_sessions)."
|
|
3964
4576
|
},
|
|
3965
|
-
daemon_id: {
|
|
3966
|
-
type: "string",
|
|
3967
|
-
description: "Daemon ID (cloud mode only, required)."
|
|
3968
|
-
},
|
|
3969
4577
|
type: {
|
|
3970
4578
|
type: "string",
|
|
3971
|
-
description: "Provider type (e.g. hermes-cli, claude-cli).
|
|
4579
|
+
description: "Provider type (e.g. hermes-cli, claude-cli). Auto-resolved from session_id if omitted."
|
|
3972
4580
|
}
|
|
3973
4581
|
},
|
|
3974
4582
|
required: ["session_id"]
|
|
3975
4583
|
}
|
|
3976
4584
|
};
|
|
3977
4585
|
async function stopSession(transport, args) {
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
}
|
|
3986
|
-
if (!resolvedType) {
|
|
3987
|
-
return `Error: could not resolve session type for ${args.session_id}. Pass type= explicitly.`;
|
|
3988
|
-
}
|
|
3989
|
-
const result2 = await local.command("stop_cli", {
|
|
3990
|
-
targetSessionId: args.session_id,
|
|
3991
|
-
cliType: resolvedType
|
|
3992
|
-
});
|
|
3993
|
-
if (result2?.success === false) return `Error: ${result2.error ?? "stop failed"}`;
|
|
3994
|
-
return `Session ${args.session_id} stopped.`;
|
|
4586
|
+
let resolvedType = args.type;
|
|
4587
|
+
if (!resolvedType) {
|
|
4588
|
+
const status = await transport.getStatus();
|
|
4589
|
+
const session = (status?.sessions ?? []).find((s) => s.id === args.session_id);
|
|
4590
|
+
resolvedType = session?.providerType ?? session?.type;
|
|
4591
|
+
}
|
|
4592
|
+
if (!resolvedType) {
|
|
4593
|
+
return `Error: could not resolve session type for ${args.session_id}. Pass type= explicitly.`;
|
|
3995
4594
|
}
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
...args.type ? { type: args.type } : {}
|
|
4595
|
+
const result = await transport.command("stop_cli", {
|
|
4596
|
+
targetSessionId: args.session_id,
|
|
4597
|
+
cliType: resolvedType
|
|
4000
4598
|
});
|
|
4001
|
-
if (result?.success === false
|
|
4599
|
+
if (result?.success === false) return `Error: ${result.error ?? "stop failed"}`;
|
|
4002
4600
|
return `Session ${args.session_id} stopped.`;
|
|
4003
4601
|
}
|
|
4004
4602
|
|
|
@@ -4009,28 +4607,18 @@ var CHECK_PENDING_TOOL = {
|
|
|
4009
4607
|
inputSchema: {
|
|
4010
4608
|
type: "object",
|
|
4011
4609
|
properties: {
|
|
4012
|
-
daemon_id: {
|
|
4013
|
-
type: "string",
|
|
4014
|
-
description: "Daemon ID to check (cloud mode). Omit to check all daemons."
|
|
4015
|
-
},
|
|
4016
4610
|
...FORMAT_PROP
|
|
4017
4611
|
},
|
|
4018
4612
|
required: []
|
|
4019
4613
|
}
|
|
4020
4614
|
};
|
|
4021
4615
|
async function checkPending(transport, args) {
|
|
4022
|
-
if (isLocalTransport(transport)) {
|
|
4023
|
-
return checkPendingLocal(transport, args.format);
|
|
4024
|
-
}
|
|
4025
|
-
return checkPendingCloud(transport, args.daemon_id, args.format);
|
|
4026
|
-
}
|
|
4027
|
-
async function checkPendingLocal(transport, format) {
|
|
4028
4616
|
const status = await transport.getStatus();
|
|
4029
4617
|
const sessions = status?.sessions ?? [];
|
|
4030
4618
|
const pending = sessions.filter(
|
|
4031
4619
|
(s) => s.status === "waiting_approval" || s.agentStatus === "waiting_approval"
|
|
4032
4620
|
);
|
|
4033
|
-
if (format === "json") {
|
|
4621
|
+
if (args.format === "json") {
|
|
4034
4622
|
return JSON.stringify({
|
|
4035
4623
|
pending: pending.map((s) => ({
|
|
4036
4624
|
session_id: s.id,
|
|
@@ -4053,56 +4641,6 @@ async function checkPendingLocal(transport, format) {
|
|
|
4053
4641
|
});
|
|
4054
4642
|
return `Pending approvals (${pending.length}):
|
|
4055
4643
|
|
|
4056
|
-
${lines.join("\n\n")}`;
|
|
4057
|
-
}
|
|
4058
|
-
async function checkPendingCloud(transport, daemonId, format) {
|
|
4059
|
-
const pending = [];
|
|
4060
|
-
if (daemonId) {
|
|
4061
|
-
const daemonStatus = await transport.getDaemonStatus(daemonId);
|
|
4062
|
-
const sessions = daemonStatus?.sessions ?? [];
|
|
4063
|
-
for (const s of sessions) {
|
|
4064
|
-
if (s.status === "waiting_approval") pending.push({ daemonId, session: s });
|
|
4065
|
-
}
|
|
4066
|
-
} else {
|
|
4067
|
-
const data = await transport.listDaemons();
|
|
4068
|
-
const daemons = data?.daemons ?? [];
|
|
4069
|
-
for (let i = 0; i < daemons.length; i += 5) {
|
|
4070
|
-
await Promise.allSettled(
|
|
4071
|
-
daemons.slice(i, i + 5).map(async (d) => {
|
|
4072
|
-
try {
|
|
4073
|
-
const daemonStatus = await transport.getDaemonStatus(d.id);
|
|
4074
|
-
const sessions = daemonStatus?.sessions ?? [];
|
|
4075
|
-
for (const s of sessions) {
|
|
4076
|
-
if (s.status === "waiting_approval") pending.push({ daemonId: d.id, session: s });
|
|
4077
|
-
}
|
|
4078
|
-
} catch {
|
|
4079
|
-
}
|
|
4080
|
-
})
|
|
4081
|
-
);
|
|
4082
|
-
}
|
|
4083
|
-
}
|
|
4084
|
-
if (format === "json") {
|
|
4085
|
-
return JSON.stringify({
|
|
4086
|
-
pending: pending.map(({ daemonId: dId, session: s }) => ({
|
|
4087
|
-
daemon_id: dId,
|
|
4088
|
-
session_id: s.id,
|
|
4089
|
-
workspace: s.workspace ?? null,
|
|
4090
|
-
type: s.providerType ?? null,
|
|
4091
|
-
modal_message: null,
|
|
4092
|
-
buttons: []
|
|
4093
|
-
}))
|
|
4094
|
-
}, null, 2);
|
|
4095
|
-
}
|
|
4096
|
-
if (pending.length === 0) return "No sessions waiting for approval.";
|
|
4097
|
-
const lines = pending.map(({ daemonId: dId, session: s }) => {
|
|
4098
|
-
const parts = [`daemon_id: ${dId}`, `session_id: ${s.id}`];
|
|
4099
|
-
if (s.workspace) parts.push(`workspace: ${s.workspace}`);
|
|
4100
|
-
if (s.providerType) parts.push(`type: ${s.providerType}`);
|
|
4101
|
-
parts.push("(use read_chat to see the approval prompt)");
|
|
4102
|
-
return parts.join("\n ");
|
|
4103
|
-
});
|
|
4104
|
-
return `Pending approvals (${pending.length}):
|
|
4105
|
-
|
|
4106
4644
|
${lines.join("\n\n")}`;
|
|
4107
4645
|
}
|
|
4108
4646
|
|
|
@@ -4116,10 +4654,10 @@ async function buildMeshModeCoordinatorPrompt(mesh) {
|
|
|
4116
4654
|
}
|
|
4117
4655
|
}
|
|
4118
4656
|
async function startMcpServer(opts) {
|
|
4119
|
-
const transport = opts.mode === "
|
|
4657
|
+
const transport = opts.mode === "ipc" ? new IpcTransport({ port: opts.port }) : new LocalTransport({ port: opts.port, password: opts.password });
|
|
4120
4658
|
const alive = await transport.ping();
|
|
4121
4659
|
if (!alive) {
|
|
4122
|
-
const hint = opts.mode === "local" ? `Make sure the standalone daemon is running (adhdev standalone or npx @adhdev/daemon-standalone).` :
|
|
4660
|
+
const hint = opts.mode === "local" ? `Make sure the standalone daemon is running (adhdev standalone or npx @adhdev/daemon-standalone).` : `Make sure the cloud daemon is running with local IPC enabled (adhdev daemon).`;
|
|
4123
4661
|
process.stderr.write(`[adhdev-mcp] Cannot reach ${opts.mode} daemon. ${hint}
|
|
4124
4662
|
`);
|
|
4125
4663
|
process.exit(1);
|
|
@@ -4134,63 +4672,6 @@ async function startMcpServer(opts) {
|
|
|
4134
4672
|
`);
|
|
4135
4673
|
} catch (e) {
|
|
4136
4674
|
process.stderr.write(`[adhdev-mcp] Failed to parse ADHDEV_INLINE_MESH: ${e.message}
|
|
4137
|
-
`);
|
|
4138
|
-
}
|
|
4139
|
-
}
|
|
4140
|
-
if (!mesh && opts.mode === "cloud" && opts.apiKey) {
|
|
4141
|
-
try {
|
|
4142
|
-
const base = opts.baseUrl || "https://api.adhf.dev";
|
|
4143
|
-
const res = await fetch(`${base}/api/v1/repo-meshes/${opts.meshId}`, {
|
|
4144
|
-
headers: { "Authorization": `Bearer ${opts.apiKey}`, "Content-Type": "application/json" }
|
|
4145
|
-
});
|
|
4146
|
-
if (res.ok) {
|
|
4147
|
-
const data = await res.json();
|
|
4148
|
-
const rm = data.mesh;
|
|
4149
|
-
const nodes = data.nodes || [];
|
|
4150
|
-
let policy = {};
|
|
4151
|
-
try {
|
|
4152
|
-
policy = JSON.parse(rm.policy_json || rm.policy || "{}");
|
|
4153
|
-
} catch {
|
|
4154
|
-
}
|
|
4155
|
-
let coordinator = {};
|
|
4156
|
-
try {
|
|
4157
|
-
coordinator = JSON.parse(rm.coordinator_json || rm.coordinator_config || "{}");
|
|
4158
|
-
} catch {
|
|
4159
|
-
}
|
|
4160
|
-
mesh = {
|
|
4161
|
-
id: rm.id,
|
|
4162
|
-
name: rm.name,
|
|
4163
|
-
repoIdentity: rm.repo_identity,
|
|
4164
|
-
repoRemoteUrl: rm.repo_remote_url,
|
|
4165
|
-
defaultBranch: rm.default_branch,
|
|
4166
|
-
policy: {
|
|
4167
|
-
requirePreTaskCheckpoint: false,
|
|
4168
|
-
requirePostTaskCheckpoint: true,
|
|
4169
|
-
requireApprovalForPush: true,
|
|
4170
|
-
requireApprovalForDestructiveGit: true,
|
|
4171
|
-
dirtyWorkspaceBehavior: "warn",
|
|
4172
|
-
maxParallelTasks: 2,
|
|
4173
|
-
spawnedSessionVisibility: "visible",
|
|
4174
|
-
...policy
|
|
4175
|
-
},
|
|
4176
|
-
coordinator,
|
|
4177
|
-
nodes: nodes.map((n) => ({
|
|
4178
|
-
id: n.id,
|
|
4179
|
-
workspace: n.workspace,
|
|
4180
|
-
repoRoot: n.repo_root,
|
|
4181
|
-
daemonId: n.daemon_id,
|
|
4182
|
-
userOverrides: {},
|
|
4183
|
-
policy: {},
|
|
4184
|
-
isLocalWorktree: false
|
|
4185
|
-
})),
|
|
4186
|
-
createdAt: rm.created_at,
|
|
4187
|
-
updatedAt: rm.updated_at
|
|
4188
|
-
};
|
|
4189
|
-
process.stderr.write(`[adhdev-mcp] Loaded mesh config from cloud API
|
|
4190
|
-
`);
|
|
4191
|
-
}
|
|
4192
|
-
} catch (e) {
|
|
4193
|
-
process.stderr.write(`[adhdev-mcp] Cloud mesh fetch failed, falling back to local: ${e.message}
|
|
4194
4675
|
`);
|
|
4195
4676
|
}
|
|
4196
4677
|
}
|
|
@@ -4217,17 +4698,19 @@ async function startMcpServer(opts) {
|
|
|
4217
4698
|
}
|
|
4218
4699
|
}
|
|
4219
4700
|
if (!mesh) {
|
|
4220
|
-
process.stderr.write(`[adhdev-mcp] Mesh '${opts.meshId}' not found in
|
|
4701
|
+
process.stderr.write(`[adhdev-mcp] Mesh '${opts.meshId}' not found in local config. Use 'adhdev mesh list' to see available meshes.
|
|
4221
4702
|
`);
|
|
4222
4703
|
process.exit(1);
|
|
4223
4704
|
}
|
|
4224
4705
|
let localDaemonId;
|
|
4225
4706
|
let localMachineId;
|
|
4707
|
+
let coordinatorHostname = import_node_os.default.hostname();
|
|
4226
4708
|
if (transport instanceof LocalTransport || transport instanceof IpcTransport) {
|
|
4227
4709
|
try {
|
|
4228
4710
|
const { loadConfig } = await import("@adhdev/daemon-core");
|
|
4229
4711
|
const cfg = loadConfig();
|
|
4230
|
-
if (cfg.
|
|
4712
|
+
if (cfg.machineId) localMachineId = cfg.machineId;
|
|
4713
|
+
else if (cfg.registeredMachineId) localMachineId = cfg.registeredMachineId;
|
|
4231
4714
|
} catch {
|
|
4232
4715
|
}
|
|
4233
4716
|
}
|
|
@@ -4235,14 +4718,16 @@ async function startMcpServer(opts) {
|
|
|
4235
4718
|
try {
|
|
4236
4719
|
const statusResult = await transport.getStatus();
|
|
4237
4720
|
const instanceId = typeof statusResult?.status?.instanceId === "string" ? statusResult.status.instanceId.trim() : "";
|
|
4721
|
+
const hostname = typeof statusResult?.status?.hostname === "string" ? statusResult.status.hostname.trim() : typeof statusResult?.status?.machine?.hostname === "string" ? statusResult.status.machine.hostname.trim() : "";
|
|
4238
4722
|
if (instanceId) localDaemonId = instanceId;
|
|
4723
|
+
if (hostname) coordinatorHostname = hostname;
|
|
4239
4724
|
} catch {
|
|
4240
4725
|
}
|
|
4241
4726
|
}
|
|
4242
|
-
const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {}, ...localMachineId ? { localMachineId } : {} };
|
|
4727
|
+
const meshCtx = { mesh, transport, ...localDaemonId ? { localDaemonId } : {}, ...localMachineId ? { localMachineId } : {}, ...coordinatorHostname ? { coordinatorHostname } : {} };
|
|
4243
4728
|
const coordinatorPrompt = await buildMeshModeCoordinatorPrompt(mesh);
|
|
4244
4729
|
const server2 = new import_server.Server(
|
|
4245
|
-
{ name: "adhdev-mcp-server", version: "0.9.
|
|
4730
|
+
{ name: "adhdev-mcp-server", version: "0.9.82" },
|
|
4246
4731
|
{ capabilities: { tools: {}, resources: {} } }
|
|
4247
4732
|
);
|
|
4248
4733
|
const { ListResourcesRequestSchema, ReadResourceRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
@@ -4268,7 +4753,7 @@ async function startMcpServer(opts) {
|
|
|
4268
4753
|
let text;
|
|
4269
4754
|
switch (name) {
|
|
4270
4755
|
case "mesh_status":
|
|
4271
|
-
text = await meshStatus(meshCtx);
|
|
4756
|
+
text = await meshStatus(meshCtx, a);
|
|
4272
4757
|
break;
|
|
4273
4758
|
case "mesh_list_nodes":
|
|
4274
4759
|
text = await meshListNodes(meshCtx);
|
|
@@ -4300,6 +4785,9 @@ async function startMcpServer(opts) {
|
|
|
4300
4785
|
case "mesh_git_status":
|
|
4301
4786
|
text = await meshGitStatus(meshCtx, a);
|
|
4302
4787
|
break;
|
|
4788
|
+
case "mesh_fast_forward_node":
|
|
4789
|
+
text = await meshFastForwardNode(meshCtx, a);
|
|
4790
|
+
break;
|
|
4303
4791
|
case "mesh_checkpoint":
|
|
4304
4792
|
text = await meshCheckpoint(meshCtx, a);
|
|
4305
4793
|
break;
|
|
@@ -4315,6 +4803,18 @@ async function startMcpServer(opts) {
|
|
|
4315
4803
|
case "mesh_refine_node":
|
|
4316
4804
|
text = await meshRefineNode(meshCtx, a);
|
|
4317
4805
|
break;
|
|
4806
|
+
case "mesh_refine_config_schema":
|
|
4807
|
+
text = await meshRefineConfigSchema(meshCtx);
|
|
4808
|
+
break;
|
|
4809
|
+
case "mesh_validate_refine_config":
|
|
4810
|
+
text = await meshValidateRefineConfig(meshCtx, a);
|
|
4811
|
+
break;
|
|
4812
|
+
case "mesh_suggest_refine_config":
|
|
4813
|
+
text = await meshSuggestRefineConfig(meshCtx, a);
|
|
4814
|
+
break;
|
|
4815
|
+
case "mesh_refine_plan":
|
|
4816
|
+
text = await meshRefinePlan(meshCtx, a);
|
|
4817
|
+
break;
|
|
4318
4818
|
case "mesh_cleanup_sessions":
|
|
4319
4819
|
text = await meshCleanupSessions(meshCtx, a);
|
|
4320
4820
|
break;
|
|
@@ -4324,6 +4824,12 @@ async function startMcpServer(opts) {
|
|
|
4324
4824
|
case "mesh_reconcile_ledger":
|
|
4325
4825
|
text = await meshReconcileLedger(meshCtx, a);
|
|
4326
4826
|
break;
|
|
4827
|
+
case "mesh_mission_upsert":
|
|
4828
|
+
text = await meshMissionUpsert(meshCtx, a);
|
|
4829
|
+
break;
|
|
4830
|
+
case "mesh_review_inbox":
|
|
4831
|
+
text = await meshReviewInbox(meshCtx, a);
|
|
4832
|
+
break;
|
|
4327
4833
|
default:
|
|
4328
4834
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
4329
4835
|
}
|
|
@@ -4346,6 +4852,7 @@ async function startMcpServer(opts) {
|
|
|
4346
4852
|
CHECK_PENDING_TOOL,
|
|
4347
4853
|
READ_CHAT_TOOL,
|
|
4348
4854
|
READ_CHAT_DEBUG_TOOL,
|
|
4855
|
+
SPEC_DEBUG_TOOL,
|
|
4349
4856
|
SEND_CHAT_TOOL,
|
|
4350
4857
|
APPROVE_TOOL,
|
|
4351
4858
|
GIT_STATUS_TOOL,
|
|
@@ -4370,7 +4877,7 @@ async function startMcpServer(opts) {
|
|
|
4370
4877
|
return { content: [{ type: "text", text }] };
|
|
4371
4878
|
}
|
|
4372
4879
|
case "list_sessions": {
|
|
4373
|
-
const text = await listSessions(transport, { format: a.format
|
|
4880
|
+
const text = await listSessions(transport, { format: a.format });
|
|
4374
4881
|
return { content: [{ type: "text", text }] };
|
|
4375
4882
|
}
|
|
4376
4883
|
case "read_chat": {
|
|
@@ -4381,13 +4888,17 @@ async function startMcpServer(opts) {
|
|
|
4381
4888
|
const text = await readChatDebug(transport, a);
|
|
4382
4889
|
return { content: [{ type: "text", text }] };
|
|
4383
4890
|
}
|
|
4891
|
+
case "spec_debug": {
|
|
4892
|
+
const text = await specDebug(transport, a);
|
|
4893
|
+
return { content: [{ type: "text", text }] };
|
|
4894
|
+
}
|
|
4384
4895
|
case "send_chat": {
|
|
4385
|
-
const text = await sendChat(transport, { message: a.message, session_id: a.session_id
|
|
4896
|
+
const text = await sendChat(transport, { message: a.message, session_id: a.session_id });
|
|
4386
4897
|
return { content: [{ type: "text", text }] };
|
|
4387
4898
|
}
|
|
4388
4899
|
case "approve": {
|
|
4389
4900
|
const action = a.action === "reject" ? "reject" : "approve";
|
|
4390
|
-
const text = await approve(transport, { action, session_id: a.session_id
|
|
4901
|
+
const text = await approve(transport, { action, session_id: a.session_id });
|
|
4391
4902
|
return { content: [{ type: "text", text }] };
|
|
4392
4903
|
}
|
|
4393
4904
|
case "screenshot": {
|
|
@@ -4400,44 +4911,42 @@ async function startMcpServer(opts) {
|
|
|
4400
4911
|
return { content: [{ type: "text", text: result.text }] };
|
|
4401
4912
|
}
|
|
4402
4913
|
case "git_status": {
|
|
4403
|
-
const text = await gitStatus(transport, { workspace: a.workspace, include_diff: a.include_diff,
|
|
4914
|
+
const text = await gitStatus(transport, { workspace: a.workspace, include_diff: a.include_diff, format: a.format });
|
|
4404
4915
|
return { content: [{ type: "text", text }] };
|
|
4405
4916
|
}
|
|
4406
4917
|
case "git_log": {
|
|
4407
|
-
const text = await gitLog(transport, { workspace: a.workspace, limit: a.limit, file: a.file, since: a.since, until: a.until,
|
|
4918
|
+
const text = await gitLog(transport, { workspace: a.workspace, limit: a.limit, file: a.file, since: a.since, until: a.until, format: a.format });
|
|
4408
4919
|
return { content: [{ type: "text", text }] };
|
|
4409
4920
|
}
|
|
4410
4921
|
case "git_diff": {
|
|
4411
|
-
const text = await gitDiff(transport, { workspace: a.workspace, file: a.file, max_lines: a.max_lines, staged: a.staged,
|
|
4922
|
+
const text = await gitDiff(transport, { workspace: a.workspace, file: a.file, max_lines: a.max_lines, staged: a.staged, format: a.format });
|
|
4412
4923
|
return { content: [{ type: "text", text }] };
|
|
4413
4924
|
}
|
|
4414
4925
|
case "git_checkpoint": {
|
|
4415
|
-
const text = await gitCheckpoint(transport, { workspace: a.workspace, message: a.message, include_untracked: a.include_untracked
|
|
4926
|
+
const text = await gitCheckpoint(transport, { workspace: a.workspace, message: a.message, include_untracked: a.include_untracked });
|
|
4416
4927
|
return { content: [{ type: "text", text }] };
|
|
4417
4928
|
}
|
|
4418
4929
|
case "git_push": {
|
|
4419
|
-
const text = await gitPush(transport, { workspace: a.workspace, remote: a.remote, branch: a.branch
|
|
4930
|
+
const text = await gitPush(transport, { workspace: a.workspace, remote: a.remote, branch: a.branch });
|
|
4420
4931
|
return { content: [{ type: "text", text }] };
|
|
4421
4932
|
}
|
|
4422
4933
|
case "launch_session": {
|
|
4423
4934
|
const text = await launchSession(transport, {
|
|
4424
4935
|
type: a.type,
|
|
4425
4936
|
workspace: a.workspace,
|
|
4426
|
-
model: a.model
|
|
4427
|
-
daemon_id: a.daemon_id
|
|
4937
|
+
model: a.model
|
|
4428
4938
|
});
|
|
4429
4939
|
return { content: [{ type: "text", text }] };
|
|
4430
4940
|
}
|
|
4431
4941
|
case "stop_session": {
|
|
4432
4942
|
const text = await stopSession(transport, {
|
|
4433
4943
|
session_id: a.session_id,
|
|
4434
|
-
daemon_id: a.daemon_id,
|
|
4435
4944
|
type: a.type
|
|
4436
4945
|
});
|
|
4437
4946
|
return { content: [{ type: "text", text }] };
|
|
4438
4947
|
}
|
|
4439
4948
|
case "check_pending": {
|
|
4440
|
-
const text = await checkPending(transport, {
|
|
4949
|
+
const text = await checkPending(transport, { format: a.format });
|
|
4441
4950
|
return { content: [{ type: "text", text }] };
|
|
4442
4951
|
}
|
|
4443
4952
|
default:
|
|
@@ -4459,26 +4968,18 @@ async function startMcpServer(opts) {
|
|
|
4459
4968
|
// src/index.ts
|
|
4460
4969
|
function parseArgs(argv, env = process.env) {
|
|
4461
4970
|
const args = argv.slice(2);
|
|
4462
|
-
let apiKey;
|
|
4463
|
-
let baseUrl;
|
|
4464
4971
|
let port;
|
|
4465
4972
|
let password;
|
|
4466
4973
|
let meshId;
|
|
4467
4974
|
let explicitMode;
|
|
4468
4975
|
for (let i = 0; i < args.length; i++) {
|
|
4469
4976
|
const arg = args[i];
|
|
4470
|
-
if (
|
|
4471
|
-
apiKey = args[++i];
|
|
4472
|
-
} else if (arg?.startsWith("--api-key=")) {
|
|
4473
|
-
apiKey = arg.slice("--api-key=".length);
|
|
4474
|
-
} else if (arg === "--base-url" && args[i + 1]) {
|
|
4475
|
-
baseUrl = args[++i];
|
|
4476
|
-
} else if (arg === "--mode" && args[i + 1]) {
|
|
4977
|
+
if (arg === "--mode" && args[i + 1]) {
|
|
4477
4978
|
const value = String(args[++i]).trim();
|
|
4478
|
-
if (value === "local" || value === "
|
|
4979
|
+
if (value === "local" || value === "ipc") explicitMode = value;
|
|
4479
4980
|
} else if (arg?.startsWith("--mode=")) {
|
|
4480
4981
|
const value = arg.slice("--mode=".length).trim();
|
|
4481
|
-
if (value === "local" || value === "
|
|
4982
|
+
if (value === "local" || value === "ipc") explicitMode = value;
|
|
4482
4983
|
} else if (arg === "--port" && args[i + 1]) {
|
|
4483
4984
|
port = Number(args[++i]);
|
|
4484
4985
|
} else if (arg?.startsWith("--port=")) {
|
|
@@ -4494,15 +4995,14 @@ function parseArgs(argv, env = process.env) {
|
|
|
4494
4995
|
process.exit(0);
|
|
4495
4996
|
}
|
|
4496
4997
|
}
|
|
4497
|
-
if (!apiKey && env.ADHDEV_API_KEY) apiKey = env.ADHDEV_API_KEY;
|
|
4498
4998
|
if (!password && env.ADHDEV_PASSWORD) password = env.ADHDEV_PASSWORD;
|
|
4499
4999
|
if (!meshId && env.ADHDEV_MESH_ID) meshId = env.ADHDEV_MESH_ID;
|
|
4500
5000
|
if (!explicitMode && env.ADHDEV_MCP_TRANSPORT) {
|
|
4501
5001
|
const value = env.ADHDEV_MCP_TRANSPORT.trim();
|
|
4502
|
-
if (value === "local" || value === "
|
|
5002
|
+
if (value === "local" || value === "ipc") explicitMode = value;
|
|
4503
5003
|
}
|
|
4504
|
-
const mode = explicitMode || (
|
|
4505
|
-
return { mode, port, password,
|
|
5004
|
+
const mode = explicitMode || (meshId && env.ADHDEV_INLINE_MESH ? "ipc" : "local");
|
|
5005
|
+
return { mode, port, password, meshId };
|
|
4506
5006
|
}
|
|
4507
5007
|
function printHelp() {
|
|
4508
5008
|
console.error(buildMcpHelpText());
|