@integrity-labs/agt-cli 0.20.9 → 0.21.0
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/bin/agt.js +3 -3
- package/dist/{chunk-UMGWKBRQ.js → chunk-NANARUEF.js} +61 -2
- package/dist/chunk-NANARUEF.js.map +1 -0
- package/dist/lib/manager-worker.js +203 -8
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/direct-chat-channel.js +12 -6
- package/mcp/slack-channel.js +12 -6
- package/mcp/telegram-channel.js +462 -2
- package/package.json +1 -1
- package/dist/chunk-UMGWKBRQ.js.map +0 -1
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
resolveChannels,
|
|
23
23
|
resolveDmTarget,
|
|
24
24
|
wrapScheduledTaskPrompt
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-NANARUEF.js";
|
|
26
26
|
import {
|
|
27
27
|
findTaskByTemplate,
|
|
28
28
|
getProjectDir,
|
|
@@ -299,6 +299,22 @@ function reapStaleMcpChildren(args) {
|
|
|
299
299
|
// src/lib/mcp-presence-reaper.ts
|
|
300
300
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
301
301
|
var DEFAULT_COLD_START_GRACE_MS = 9e4;
|
|
302
|
+
var MAX_PRESENCE_RESTART_ATTEMPTS = 3;
|
|
303
|
+
var presenceReaperState = /* @__PURE__ */ new Map();
|
|
304
|
+
function stateKey(codeName, serverKey) {
|
|
305
|
+
return `${codeName}\0${serverKey}`;
|
|
306
|
+
}
|
|
307
|
+
function clearPresenceReaperState(codeName) {
|
|
308
|
+
const prefix = `${codeName}\0`;
|
|
309
|
+
for (const key of presenceReaperState.keys()) {
|
|
310
|
+
if (key.startsWith(prefix)) presenceReaperState.delete(key);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function clearPresenceReaperStateForKeys(codeName, keys) {
|
|
314
|
+
for (const key of keys) {
|
|
315
|
+
presenceReaperState.delete(stateKey(codeName, key));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
302
318
|
function findMissingMcpServers(args) {
|
|
303
319
|
const { rows, codeName, mcpJson } = args;
|
|
304
320
|
const servers = mcpJson?.mcpServers ?? {};
|
|
@@ -326,7 +342,9 @@ function reapMissingMcpSessions(args) {
|
|
|
326
342
|
mcpJson,
|
|
327
343
|
sessionStartedAt,
|
|
328
344
|
stopSession,
|
|
329
|
-
graceMs = DEFAULT_COLD_START_GRACE_MS
|
|
345
|
+
graceMs = DEFAULT_COLD_START_GRACE_MS,
|
|
346
|
+
onGiveUp,
|
|
347
|
+
onRestart
|
|
330
348
|
} = args;
|
|
331
349
|
const now = args.now ?? Date.now;
|
|
332
350
|
const runPs = args.runPs ?? (() => execFileSync2("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", timeout: 5e3 }));
|
|
@@ -352,14 +370,86 @@ function reapMissingMcpSessions(args) {
|
|
|
352
370
|
}
|
|
353
371
|
const rows = parsePsRows(psOutput);
|
|
354
372
|
const missing = findMissingMcpServers({ rows, codeName, mcpJson });
|
|
373
|
+
const declared = mcpJson.mcpServers ?? {};
|
|
374
|
+
const liveKeys = /* @__PURE__ */ new Set();
|
|
375
|
+
for (const key of Object.keys(declared)) {
|
|
376
|
+
const entry = declared[key];
|
|
377
|
+
if (typeof entry?.command !== "string") continue;
|
|
378
|
+
if (!missing.includes(key)) liveKeys.add(key);
|
|
379
|
+
}
|
|
380
|
+
for (const key of liveKeys) {
|
|
381
|
+
const state2 = presenceReaperState.get(stateKey(codeName, key));
|
|
382
|
+
if (state2) {
|
|
383
|
+
state2.attempts = 0;
|
|
384
|
+
state2.lastSeenLiveAt = now();
|
|
385
|
+
state2.lastAttemptedSessionStartedAt = null;
|
|
386
|
+
state2.gaveUpLogged = false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
355
389
|
if (missing.length === 0) {
|
|
356
390
|
return { missing, restarted: false, reason: "healthy" };
|
|
357
391
|
}
|
|
392
|
+
const givenUp = [];
|
|
393
|
+
const active = [];
|
|
394
|
+
for (const key of missing) {
|
|
395
|
+
const sk = stateKey(codeName, key);
|
|
396
|
+
const state2 = presenceReaperState.get(sk) ?? {
|
|
397
|
+
attempts: 0,
|
|
398
|
+
lastSeenLiveAt: null,
|
|
399
|
+
lastAttemptedSessionStartedAt: null,
|
|
400
|
+
gaveUpLogged: false
|
|
401
|
+
};
|
|
402
|
+
if (state2.lastAttemptedSessionStartedAt !== sessionStartedAt) {
|
|
403
|
+
state2.attempts += 1;
|
|
404
|
+
state2.lastAttemptedSessionStartedAt = sessionStartedAt;
|
|
405
|
+
}
|
|
406
|
+
presenceReaperState.set(sk, state2);
|
|
407
|
+
if (state2.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
|
|
408
|
+
givenUp.push(key);
|
|
409
|
+
if (!state2.gaveUpLogged) {
|
|
410
|
+
log2(
|
|
411
|
+
`[mcp-presence-reaper] giving up on '${codeName}:${key}' after ${MAX_PRESENCE_RESTART_ATTEMPTS} consecutive failed restarts \u2014 declared but never recovers (likely orphaned config; see ENG-5279)`
|
|
412
|
+
);
|
|
413
|
+
state2.gaveUpLogged = true;
|
|
414
|
+
if (onGiveUp) {
|
|
415
|
+
try {
|
|
416
|
+
onGiveUp(codeName, key);
|
|
417
|
+
} catch (err) {
|
|
418
|
+
log2(
|
|
419
|
+
`[mcp-presence-reaper] onGiveUp callback threw for '${codeName}:${key}' (suppressed; reaper continues): ${err.message}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
active.push(key);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (active.length === 0) {
|
|
429
|
+
return { missing, restarted: false, reason: "all-keys-over-cap", givenUp };
|
|
430
|
+
}
|
|
431
|
+
const givenUpSuffix = givenUp.length > 0 ? ` (skipping over-cap: [${givenUp.join(", ")}])` : "";
|
|
358
432
|
log2(
|
|
359
|
-
`[mcp-presence-reaper] '${codeName}': declared MCP(s) [${
|
|
433
|
+
`[mcp-presence-reaper] '${codeName}': declared MCP(s) [${active.join(", ")}] have no live children \u2014 restarting session${givenUpSuffix}`
|
|
360
434
|
);
|
|
435
|
+
if (onRestart) {
|
|
436
|
+
try {
|
|
437
|
+
const maybePromise = onRestart(codeName, { activeKeys: active, givenUpKeys: givenUp });
|
|
438
|
+
if (maybePromise && typeof maybePromise.then === "function") {
|
|
439
|
+
void maybePromise.then(void 0, (err) => {
|
|
440
|
+
log2(
|
|
441
|
+
`[mcp-presence-reaper] onRestart callback rejected for '${codeName}' (suppressed; restart proceeds): ${err.message}`
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
log2(
|
|
447
|
+
`[mcp-presence-reaper] onRestart callback threw for '${codeName}' (suppressed; restart proceeds): ${err.message}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
361
451
|
stopSession(codeName);
|
|
362
|
-
return { missing, restarted: true };
|
|
452
|
+
return { missing, restarted: true, givenUp: givenUp.length > 0 ? givenUp : void 0 };
|
|
363
453
|
}
|
|
364
454
|
|
|
365
455
|
// src/lib/channel-restart-decision.ts
|
|
@@ -2094,6 +2184,7 @@ function cancelPendingSessionRestart(codeName) {
|
|
|
2094
2184
|
var writtenHashes = /* @__PURE__ */ new Map();
|
|
2095
2185
|
var knownSecretsHashes = /* @__PURE__ */ new Map();
|
|
2096
2186
|
var runningMcpHashes = /* @__PURE__ */ new Map();
|
|
2187
|
+
var runningMcpServerKeys = /* @__PURE__ */ new Map();
|
|
2097
2188
|
function projectMcpHash(_codeName, projectDir) {
|
|
2098
2189
|
try {
|
|
2099
2190
|
const raw = readFileSync3(join4(projectDir, ".mcp.json"), "utf-8");
|
|
@@ -2102,10 +2193,22 @@ function projectMcpHash(_codeName, projectDir) {
|
|
|
2102
2193
|
return null;
|
|
2103
2194
|
}
|
|
2104
2195
|
}
|
|
2196
|
+
function projectMcpKeys(_codeName, projectDir) {
|
|
2197
|
+
try {
|
|
2198
|
+
const raw = readFileSync3(join4(projectDir, ".mcp.json"), "utf-8");
|
|
2199
|
+
const parsed = JSON.parse(raw);
|
|
2200
|
+
const servers = parsed.mcpServers;
|
|
2201
|
+
if (!servers || typeof servers !== "object") return /* @__PURE__ */ new Set();
|
|
2202
|
+
return new Set(Object.keys(servers));
|
|
2203
|
+
} catch {
|
|
2204
|
+
return null;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2105
2207
|
function stopPersistentSessionAndForgetMcpBaseline(codeName) {
|
|
2106
2208
|
cancelPendingSessionRestart(codeName);
|
|
2107
2209
|
stopPersistentSession(codeName, log);
|
|
2108
2210
|
runningMcpHashes.delete(codeName);
|
|
2211
|
+
runningMcpServerKeys.delete(codeName);
|
|
2109
2212
|
}
|
|
2110
2213
|
function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
|
|
2111
2214
|
const currentHash = projectMcpHash(codeName, projectDir);
|
|
@@ -2114,16 +2217,47 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
|
|
|
2114
2217
|
case "no-config":
|
|
2115
2218
|
case "no-drift":
|
|
2116
2219
|
return;
|
|
2117
|
-
case "baseline":
|
|
2220
|
+
case "baseline": {
|
|
2118
2221
|
runningMcpHashes.set(codeName, action.hash);
|
|
2222
|
+
const baselineKeys = projectMcpKeys(codeName, projectDir);
|
|
2223
|
+
if (baselineKeys) {
|
|
2224
|
+
runningMcpServerKeys.set(codeName, baselineKeys);
|
|
2225
|
+
} else {
|
|
2226
|
+
runningMcpServerKeys.delete(codeName);
|
|
2227
|
+
}
|
|
2119
2228
|
return;
|
|
2120
|
-
|
|
2229
|
+
}
|
|
2230
|
+
case "drift": {
|
|
2121
2231
|
log(
|
|
2122
2232
|
`[hot-reload] .mcp.json content changed for '${codeName}' (${action.previous.slice(0, 12)} \u2192 ${action.current.slice(0, 12)}); scheduling restart (ENG-4897)`
|
|
2123
2233
|
);
|
|
2234
|
+
const previousKeys = runningMcpServerKeys.get(codeName);
|
|
2235
|
+
const currentKeys = projectMcpKeys(codeName, projectDir);
|
|
2236
|
+
if (!previousKeys || !currentKeys) {
|
|
2237
|
+
clearPresenceReaperState(codeName);
|
|
2238
|
+
log(
|
|
2239
|
+
`[hot-reload] .mcp.json key membership unavailable for '${codeName}' \u2014 clearing full presence-reaper state (ENG-5285)`
|
|
2240
|
+
);
|
|
2241
|
+
} else {
|
|
2242
|
+
const addedOrRemoved = /* @__PURE__ */ new Set();
|
|
2243
|
+
for (const k of currentKeys) if (!previousKeys.has(k)) addedOrRemoved.add(k);
|
|
2244
|
+
for (const k of previousKeys) if (!currentKeys.has(k)) addedOrRemoved.add(k);
|
|
2245
|
+
if (addedOrRemoved.size > 0) {
|
|
2246
|
+
clearPresenceReaperStateForKeys(codeName, addedOrRemoved);
|
|
2247
|
+
log(
|
|
2248
|
+
`[hot-reload] .mcp.json membership changed for '${codeName}': ${addedOrRemoved.size} key(s) added/removed [${Array.from(addedOrRemoved).sort().join(", ")}] \u2014 reaper state cleared for those keys only (ENG-5285)`
|
|
2249
|
+
);
|
|
2250
|
+
} else {
|
|
2251
|
+
log(
|
|
2252
|
+
`[hot-reload] .mcp.json value-only drift for '${codeName}' \u2014 preserving presence-reaper state for the unchanged declared key set (ENG-5285)`
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2124
2256
|
scheduleSessionRestart(codeName, 0, ".mcp.json content change (ENG-4897)");
|
|
2125
2257
|
runningMcpHashes.delete(codeName);
|
|
2258
|
+
runningMcpServerKeys.delete(codeName);
|
|
2126
2259
|
return;
|
|
2260
|
+
}
|
|
2127
2261
|
}
|
|
2128
2262
|
}
|
|
2129
2263
|
var knownChannelConfigHashes = /* @__PURE__ */ new Map();
|
|
@@ -2132,6 +2266,10 @@ var knownTasksHashes = /* @__PURE__ */ new Map();
|
|
|
2132
2266
|
var knownIntegrationHashes = /* @__PURE__ */ new Map();
|
|
2133
2267
|
var knownManagedMcpHashes = /* @__PURE__ */ new Map();
|
|
2134
2268
|
var knownSkillHashes = /* @__PURE__ */ new Map();
|
|
2269
|
+
var managedToolkitIdByAgentAndServerId = /* @__PURE__ */ new Map();
|
|
2270
|
+
function managedToolkitMapKey(agentId, serverId) {
|
|
2271
|
+
return `${agentId}\0${serverId}`;
|
|
2272
|
+
}
|
|
2135
2273
|
var lastCronRunTs = /* @__PURE__ */ new Map();
|
|
2136
2274
|
var lastWorkTriggerAt = /* @__PURE__ */ new Map();
|
|
2137
2275
|
var alertedStaleItems = /* @__PURE__ */ new Set();
|
|
@@ -2200,7 +2338,7 @@ function clearAgentCaches(agentId, codeName) {
|
|
|
2200
2338
|
var cachedFrameworkVersion = null;
|
|
2201
2339
|
var lastVersionCheckAt = 0;
|
|
2202
2340
|
var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2203
|
-
var agtCliVersion = true ? "0.
|
|
2341
|
+
var agtCliVersion = true ? "0.21.0" : "dev";
|
|
2204
2342
|
function resolveBrewPath(execFileSync4) {
|
|
2205
2343
|
try {
|
|
2206
2344
|
const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
@@ -4011,6 +4149,10 @@ async function processAgent(agent, agentStates) {
|
|
|
4011
4149
|
for (const tk of agentToolkits) {
|
|
4012
4150
|
const serverId = tk.toolkit_id.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
4013
4151
|
expectedServerIds.add(serverId);
|
|
4152
|
+
managedToolkitIdByAgentAndServerId.set(
|
|
4153
|
+
managedToolkitMapKey(agent.agent_id, serverId),
|
|
4154
|
+
tk.toolkit_id
|
|
4155
|
+
);
|
|
4014
4156
|
const mcpUrl = tk.mcp_url;
|
|
4015
4157
|
const mcpHeaders = tk.mcp_headers;
|
|
4016
4158
|
const proxyUrl = tk.proxy_url.startsWith("/") ? `${requireHost()}${tk.proxy_url}` : tk.proxy_url;
|
|
@@ -4362,7 +4504,60 @@ async function processAgent(agent, agentStates) {
|
|
|
4362
4504
|
codeName: agent.code_name,
|
|
4363
4505
|
mcpJson: mcpJsonParsed,
|
|
4364
4506
|
sessionStartedAt: sess?.startedAt ?? null,
|
|
4365
|
-
stopSession: (codeName) => stopPersistentSessionAndForgetMcpBaseline(codeName)
|
|
4507
|
+
stopSession: (codeName) => stopPersistentSessionAndForgetMcpBaseline(codeName),
|
|
4508
|
+
// ENG-5292: when the reaper gives up on a managed MCP (cap from
|
|
4509
|
+
// ENG-5279 + state-preservation from ENG-5285 both said "this
|
|
4510
|
+
// MCP keeps failing after 3 restart cycles"), mark the matching
|
|
4511
|
+
// integration unhealthy server-side. The next /host/managed-
|
|
4512
|
+
// toolkits poll drops it from the response → next writeMcpServer
|
|
4513
|
+
// omits it → broken MCP is cleanly gone instead of declared-but-
|
|
4514
|
+
// dead.
|
|
4515
|
+
//
|
|
4516
|
+
// Async fire-and-forget — the reaper's correctness must not
|
|
4517
|
+
// depend on the API call succeeding. Errors are caught inside
|
|
4518
|
+
// the .then/.catch chain and logged.
|
|
4519
|
+
onGiveUp: (codeName, serverKey) => {
|
|
4520
|
+
const toolkitId = managedToolkitIdByAgentAndServerId.get(
|
|
4521
|
+
managedToolkitMapKey(agent.agent_id, serverKey)
|
|
4522
|
+
);
|
|
4523
|
+
if (!toolkitId) {
|
|
4524
|
+
log(
|
|
4525
|
+
`[mcp-presence-reaper] give-up on '${codeName}:${serverKey}' \u2014 not a managed toolkit (no toolkit_id mapping), skipping integration-health API call`
|
|
4526
|
+
);
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
const agentId = agent.agent_id;
|
|
4530
|
+
api.post("/host/integration-health", {
|
|
4531
|
+
agent_id: agentId,
|
|
4532
|
+
toolkit_id: toolkitId,
|
|
4533
|
+
action: "mark_unhealthy",
|
|
4534
|
+
reason: `mcp-presence-reaper cap (${3} consecutive failed restart cycles) tripped on '${serverKey}'`
|
|
4535
|
+
}).then((res) => {
|
|
4536
|
+
log(
|
|
4537
|
+
`[mcp-presence-reaper] marked integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${JSON.stringify(res).slice(0, 200)}`
|
|
4538
|
+
);
|
|
4539
|
+
}).catch((err) => {
|
|
4540
|
+
log(
|
|
4541
|
+
`[mcp-presence-reaper] failed to mark integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${err.message} \u2014 local give-up state still recorded; will retry on next agent provisioning cycle if .mcp.json changes`
|
|
4542
|
+
);
|
|
4543
|
+
});
|
|
4544
|
+
},
|
|
4545
|
+
// ENG-5286: record the restart event with the API so the
|
|
4546
|
+
// agent-heartbeat-monitor cron can aggregate per-agent counts
|
|
4547
|
+
// into the McpReaperRestartsHourly CloudWatch metric. Fire-and-
|
|
4548
|
+
// forget — the reaper's correctness must not depend on this.
|
|
4549
|
+
onRestart: (codeName, { activeKeys, givenUpKeys }) => {
|
|
4550
|
+
api.post("/host/mcp-reaper-restart", {
|
|
4551
|
+
agent_id: agent.agent_id,
|
|
4552
|
+
code_name: codeName,
|
|
4553
|
+
server_keys: activeKeys,
|
|
4554
|
+
given_up_keys: givenUpKeys
|
|
4555
|
+
}).catch((err) => {
|
|
4556
|
+
log(
|
|
4557
|
+
`[mcp-presence-reaper] failed to record restart event for '${codeName}' (ENG-5286): ${err.message} \u2014 local restart still proceeded; CloudWatch metric will under-count this event`
|
|
4558
|
+
);
|
|
4559
|
+
});
|
|
4560
|
+
}
|
|
4366
4561
|
});
|
|
4367
4562
|
} catch (err) {
|
|
4368
4563
|
log(`[mcp-presence-reaper] presence check failed for '${agent.code_name}': ${err.message}`);
|