@integrity-labs/agt-cli 0.20.9 → 0.21.1

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.
@@ -22,7 +22,7 @@ import {
22
22
  resolveChannels,
23
23
  resolveDmTarget,
24
24
  wrapScheduledTaskPrompt
25
- } from "../chunk-UMGWKBRQ.js";
25
+ } from "../chunk-NLEW5BPX.js";
26
26
  import {
27
27
  findTaskByTemplate,
28
28
  getProjectDir,
@@ -92,6 +92,25 @@ function computeIntegrationsHash(integrations) {
92
92
 
93
93
  // src/lib/stale-mcp-reaper.ts
94
94
  import { execFileSync } from "child_process";
95
+ var rotationTimestamps = /* @__PURE__ */ new Map();
96
+ var DEFAULT_ROTATION_GRACE_MS = 15e3;
97
+ function rotationKey(codeName, serverKey) {
98
+ return `${codeName}\0${serverKey}`;
99
+ }
100
+ function recordStaleRotation(codeName, serverKeys, now = Date.now) {
101
+ const ts = now();
102
+ for (const key of serverKeys) {
103
+ rotationTimestamps.set(rotationKey(codeName, key), ts);
104
+ }
105
+ }
106
+ function wasRecentlyRotated(codeName, serverKey, withinMs, now = Date.now) {
107
+ const k = rotationKey(codeName, serverKey);
108
+ const ts = rotationTimestamps.get(k);
109
+ if (ts === void 0) return false;
110
+ if (now() - ts < withinMs) return true;
111
+ rotationTimestamps.delete(k);
112
+ return false;
113
+ }
95
114
  function parseEnvIntegrationsEntries(content) {
96
115
  const entries = /* @__PURE__ */ new Map();
97
116
  for (const raw of content.split(/\r?\n/)) {
@@ -264,6 +283,16 @@ function reapStaleMcpChildren(args) {
264
283
  for (const pid of targets) {
265
284
  killProcess(pid, "SIGTERM");
266
285
  }
286
+ const killedKeys = /* @__PURE__ */ new Set();
287
+ for (const pid of targets) {
288
+ const argv = byPid.get(pid)?.args ?? "";
289
+ for (const key of serverKeys) {
290
+ const entry = mcpJson?.mcpServers?.[key];
291
+ const matchers = buildArgvMatchersForEntry(key, entry);
292
+ if (matchers.some((re) => re.test(argv))) killedKeys.add(key);
293
+ }
294
+ }
295
+ if (killedKeys.size > 0) recordStaleRotation(codeName, killedKeys);
267
296
  setTimeout(() => {
268
297
  try {
269
298
  let freshPsOutput;
@@ -299,6 +328,22 @@ function reapStaleMcpChildren(args) {
299
328
  // src/lib/mcp-presence-reaper.ts
300
329
  import { execFileSync as execFileSync2 } from "child_process";
301
330
  var DEFAULT_COLD_START_GRACE_MS = 9e4;
331
+ var MAX_PRESENCE_RESTART_ATTEMPTS = 3;
332
+ var presenceReaperState = /* @__PURE__ */ new Map();
333
+ function stateKey(codeName, serverKey) {
334
+ return `${codeName}\0${serverKey}`;
335
+ }
336
+ function clearPresenceReaperState(codeName) {
337
+ const prefix = `${codeName}\0`;
338
+ for (const key of presenceReaperState.keys()) {
339
+ if (key.startsWith(prefix)) presenceReaperState.delete(key);
340
+ }
341
+ }
342
+ function clearPresenceReaperStateForKeys(codeName, keys) {
343
+ for (const key of keys) {
344
+ presenceReaperState.delete(stateKey(codeName, key));
345
+ }
346
+ }
302
347
  function findMissingMcpServers(args) {
303
348
  const { rows, codeName, mcpJson } = args;
304
349
  const servers = mcpJson?.mcpServers ?? {};
@@ -326,7 +371,10 @@ function reapMissingMcpSessions(args) {
326
371
  mcpJson,
327
372
  sessionStartedAt,
328
373
  stopSession,
329
- graceMs = DEFAULT_COLD_START_GRACE_MS
374
+ graceMs = DEFAULT_COLD_START_GRACE_MS,
375
+ rotationGraceMs = DEFAULT_ROTATION_GRACE_MS,
376
+ onGiveUp,
377
+ onRestart
330
378
  } = args;
331
379
  const now = args.now ?? Date.now;
332
380
  const runPs = args.runPs ?? (() => execFileSync2("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", timeout: 5e3 }));
@@ -351,15 +399,117 @@ function reapMissingMcpSessions(args) {
351
399
  return { missing: [], restarted: false, reason: "healthy" };
352
400
  }
353
401
  const rows = parsePsRows(psOutput);
354
- const missing = findMissingMcpServers({ rows, codeName, mcpJson });
402
+ const missingRaw = findMissingMcpServers({ rows, codeName, mcpJson });
403
+ const rotationGraced = [];
404
+ const missing = [];
405
+ for (const key of missingRaw) {
406
+ if (wasRecentlyRotated(codeName, key, rotationGraceMs, now)) {
407
+ rotationGraced.push(key);
408
+ } else {
409
+ missing.push(key);
410
+ }
411
+ }
412
+ if (rotationGraced.length > 0) {
413
+ log2(
414
+ `[mcp-presence-reaper] '${codeName}': skipping [${rotationGraced.join(", ")}] within rotation grace window (${rotationGraceMs}ms) \u2014 child(ren) just SIGTERM'd by stale-mcp-reaper, awaiting respawn on next tool call (ENG-5344)`
415
+ );
416
+ }
417
+ const declared = mcpJson.mcpServers ?? {};
418
+ const liveKeys = /* @__PURE__ */ new Set();
419
+ for (const key of Object.keys(declared)) {
420
+ const entry = declared[key];
421
+ if (typeof entry?.command !== "string") continue;
422
+ if (!missingRaw.includes(key)) liveKeys.add(key);
423
+ }
424
+ for (const key of liveKeys) {
425
+ const state2 = presenceReaperState.get(stateKey(codeName, key));
426
+ if (state2) {
427
+ state2.attempts = 0;
428
+ state2.lastSeenLiveAt = now();
429
+ state2.lastAttemptedSessionStartedAt = null;
430
+ state2.gaveUpLogged = false;
431
+ }
432
+ }
355
433
  if (missing.length === 0) {
356
- return { missing, restarted: false, reason: "healthy" };
434
+ return {
435
+ missing,
436
+ restarted: false,
437
+ reason: "healthy",
438
+ rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
439
+ };
440
+ }
441
+ const givenUp = [];
442
+ const active = [];
443
+ for (const key of missing) {
444
+ const sk = stateKey(codeName, key);
445
+ const state2 = presenceReaperState.get(sk) ?? {
446
+ attempts: 0,
447
+ lastSeenLiveAt: null,
448
+ lastAttemptedSessionStartedAt: null,
449
+ gaveUpLogged: false
450
+ };
451
+ if (state2.lastAttemptedSessionStartedAt !== sessionStartedAt) {
452
+ state2.attempts += 1;
453
+ state2.lastAttemptedSessionStartedAt = sessionStartedAt;
454
+ }
455
+ presenceReaperState.set(sk, state2);
456
+ if (state2.attempts > MAX_PRESENCE_RESTART_ATTEMPTS) {
457
+ givenUp.push(key);
458
+ if (!state2.gaveUpLogged) {
459
+ log2(
460
+ `[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)`
461
+ );
462
+ state2.gaveUpLogged = true;
463
+ if (onGiveUp) {
464
+ try {
465
+ onGiveUp(codeName, key);
466
+ } catch (err) {
467
+ log2(
468
+ `[mcp-presence-reaper] onGiveUp callback threw for '${codeName}:${key}' (suppressed; reaper continues): ${err.message}`
469
+ );
470
+ }
471
+ }
472
+ }
473
+ } else {
474
+ active.push(key);
475
+ }
476
+ }
477
+ if (active.length === 0) {
478
+ return {
479
+ missing,
480
+ restarted: false,
481
+ reason: "all-keys-over-cap",
482
+ givenUp,
483
+ rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
484
+ };
357
485
  }
486
+ const givenUpSuffix = givenUp.length > 0 ? ` (skipping over-cap: [${givenUp.join(", ")}])` : "";
358
487
  log2(
359
- `[mcp-presence-reaper] '${codeName}': declared MCP(s) [${missing.join(", ")}] have no live children \u2014 restarting session`
488
+ `[mcp-presence-reaper] '${codeName}': declared MCP(s) [${active.join(", ")}] have no live children \u2014 restarting session${givenUpSuffix}`
360
489
  );
490
+ if (onRestart) {
491
+ try {
492
+ const maybePromise = onRestart(codeName, { activeKeys: active, givenUpKeys: givenUp });
493
+ if (maybePromise && typeof maybePromise.then === "function") {
494
+ void maybePromise.then(void 0, (err) => {
495
+ log2(
496
+ `[mcp-presence-reaper] onRestart callback rejected for '${codeName}' (suppressed; restart proceeds): ${err.message}`
497
+ );
498
+ });
499
+ }
500
+ } catch (err) {
501
+ log2(
502
+ `[mcp-presence-reaper] onRestart callback threw for '${codeName}' (suppressed; restart proceeds): ${err.message}`
503
+ );
504
+ }
505
+ }
361
506
  stopSession(codeName);
362
- return { missing, restarted: true };
507
+ return {
508
+ missing,
509
+ restarted: true,
510
+ givenUp: givenUp.length > 0 ? givenUp : void 0,
511
+ rotationGraced: rotationGraced.length > 0 ? rotationGraced : void 0
512
+ };
363
513
  }
364
514
 
365
515
  // src/lib/channel-restart-decision.ts
@@ -2094,6 +2244,7 @@ function cancelPendingSessionRestart(codeName) {
2094
2244
  var writtenHashes = /* @__PURE__ */ new Map();
2095
2245
  var knownSecretsHashes = /* @__PURE__ */ new Map();
2096
2246
  var runningMcpHashes = /* @__PURE__ */ new Map();
2247
+ var runningMcpServerKeys = /* @__PURE__ */ new Map();
2097
2248
  function projectMcpHash(_codeName, projectDir) {
2098
2249
  try {
2099
2250
  const raw = readFileSync3(join4(projectDir, ".mcp.json"), "utf-8");
@@ -2102,10 +2253,22 @@ function projectMcpHash(_codeName, projectDir) {
2102
2253
  return null;
2103
2254
  }
2104
2255
  }
2256
+ function projectMcpKeys(_codeName, projectDir) {
2257
+ try {
2258
+ const raw = readFileSync3(join4(projectDir, ".mcp.json"), "utf-8");
2259
+ const parsed = JSON.parse(raw);
2260
+ const servers = parsed.mcpServers;
2261
+ if (!servers || typeof servers !== "object") return /* @__PURE__ */ new Set();
2262
+ return new Set(Object.keys(servers));
2263
+ } catch {
2264
+ return null;
2265
+ }
2266
+ }
2105
2267
  function stopPersistentSessionAndForgetMcpBaseline(codeName) {
2106
2268
  cancelPendingSessionRestart(codeName);
2107
2269
  stopPersistentSession(codeName, log);
2108
2270
  runningMcpHashes.delete(codeName);
2271
+ runningMcpServerKeys.delete(codeName);
2109
2272
  }
2110
2273
  function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
2111
2274
  const currentHash = projectMcpHash(codeName, projectDir);
@@ -2114,16 +2277,47 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
2114
2277
  case "no-config":
2115
2278
  case "no-drift":
2116
2279
  return;
2117
- case "baseline":
2280
+ case "baseline": {
2118
2281
  runningMcpHashes.set(codeName, action.hash);
2282
+ const baselineKeys = projectMcpKeys(codeName, projectDir);
2283
+ if (baselineKeys) {
2284
+ runningMcpServerKeys.set(codeName, baselineKeys);
2285
+ } else {
2286
+ runningMcpServerKeys.delete(codeName);
2287
+ }
2119
2288
  return;
2120
- case "drift":
2289
+ }
2290
+ case "drift": {
2121
2291
  log(
2122
2292
  `[hot-reload] .mcp.json content changed for '${codeName}' (${action.previous.slice(0, 12)} \u2192 ${action.current.slice(0, 12)}); scheduling restart (ENG-4897)`
2123
2293
  );
2294
+ const previousKeys = runningMcpServerKeys.get(codeName);
2295
+ const currentKeys = projectMcpKeys(codeName, projectDir);
2296
+ if (!previousKeys || !currentKeys) {
2297
+ clearPresenceReaperState(codeName);
2298
+ log(
2299
+ `[hot-reload] .mcp.json key membership unavailable for '${codeName}' \u2014 clearing full presence-reaper state (ENG-5285)`
2300
+ );
2301
+ } else {
2302
+ const addedOrRemoved = /* @__PURE__ */ new Set();
2303
+ for (const k of currentKeys) if (!previousKeys.has(k)) addedOrRemoved.add(k);
2304
+ for (const k of previousKeys) if (!currentKeys.has(k)) addedOrRemoved.add(k);
2305
+ if (addedOrRemoved.size > 0) {
2306
+ clearPresenceReaperStateForKeys(codeName, addedOrRemoved);
2307
+ log(
2308
+ `[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)`
2309
+ );
2310
+ } else {
2311
+ log(
2312
+ `[hot-reload] .mcp.json value-only drift for '${codeName}' \u2014 preserving presence-reaper state for the unchanged declared key set (ENG-5285)`
2313
+ );
2314
+ }
2315
+ }
2124
2316
  scheduleSessionRestart(codeName, 0, ".mcp.json content change (ENG-4897)");
2125
2317
  runningMcpHashes.delete(codeName);
2318
+ runningMcpServerKeys.delete(codeName);
2126
2319
  return;
2320
+ }
2127
2321
  }
2128
2322
  }
2129
2323
  var knownChannelConfigHashes = /* @__PURE__ */ new Map();
@@ -2132,6 +2326,10 @@ var knownTasksHashes = /* @__PURE__ */ new Map();
2132
2326
  var knownIntegrationHashes = /* @__PURE__ */ new Map();
2133
2327
  var knownManagedMcpHashes = /* @__PURE__ */ new Map();
2134
2328
  var knownSkillHashes = /* @__PURE__ */ new Map();
2329
+ var managedToolkitIdByAgentAndServerId = /* @__PURE__ */ new Map();
2330
+ function managedToolkitMapKey(agentId, serverId) {
2331
+ return `${agentId}\0${serverId}`;
2332
+ }
2135
2333
  var lastCronRunTs = /* @__PURE__ */ new Map();
2136
2334
  var lastWorkTriggerAt = /* @__PURE__ */ new Map();
2137
2335
  var alertedStaleItems = /* @__PURE__ */ new Set();
@@ -2200,7 +2398,7 @@ function clearAgentCaches(agentId, codeName) {
2200
2398
  var cachedFrameworkVersion = null;
2201
2399
  var lastVersionCheckAt = 0;
2202
2400
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
2203
- var agtCliVersion = true ? "0.20.9" : "dev";
2401
+ var agtCliVersion = true ? "0.21.1" : "dev";
2204
2402
  function resolveBrewPath(execFileSync4) {
2205
2403
  try {
2206
2404
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -4011,6 +4209,10 @@ async function processAgent(agent, agentStates) {
4011
4209
  for (const tk of agentToolkits) {
4012
4210
  const serverId = tk.toolkit_id.replace(/[^a-z0-9]/gi, "_").toLowerCase();
4013
4211
  expectedServerIds.add(serverId);
4212
+ managedToolkitIdByAgentAndServerId.set(
4213
+ managedToolkitMapKey(agent.agent_id, serverId),
4214
+ tk.toolkit_id
4215
+ );
4014
4216
  const mcpUrl = tk.mcp_url;
4015
4217
  const mcpHeaders = tk.mcp_headers;
4016
4218
  const proxyUrl = tk.proxy_url.startsWith("/") ? `${requireHost()}${tk.proxy_url}` : tk.proxy_url;
@@ -4362,7 +4564,60 @@ async function processAgent(agent, agentStates) {
4362
4564
  codeName: agent.code_name,
4363
4565
  mcpJson: mcpJsonParsed,
4364
4566
  sessionStartedAt: sess?.startedAt ?? null,
4365
- stopSession: (codeName) => stopPersistentSessionAndForgetMcpBaseline(codeName)
4567
+ stopSession: (codeName) => stopPersistentSessionAndForgetMcpBaseline(codeName),
4568
+ // ENG-5292: when the reaper gives up on a managed MCP (cap from
4569
+ // ENG-5279 + state-preservation from ENG-5285 both said "this
4570
+ // MCP keeps failing after 3 restart cycles"), mark the matching
4571
+ // integration unhealthy server-side. The next /host/managed-
4572
+ // toolkits poll drops it from the response → next writeMcpServer
4573
+ // omits it → broken MCP is cleanly gone instead of declared-but-
4574
+ // dead.
4575
+ //
4576
+ // Async fire-and-forget — the reaper's correctness must not
4577
+ // depend on the API call succeeding. Errors are caught inside
4578
+ // the .then/.catch chain and logged.
4579
+ onGiveUp: (codeName, serverKey) => {
4580
+ const toolkitId = managedToolkitIdByAgentAndServerId.get(
4581
+ managedToolkitMapKey(agent.agent_id, serverKey)
4582
+ );
4583
+ if (!toolkitId) {
4584
+ log(
4585
+ `[mcp-presence-reaper] give-up on '${codeName}:${serverKey}' \u2014 not a managed toolkit (no toolkit_id mapping), skipping integration-health API call`
4586
+ );
4587
+ return;
4588
+ }
4589
+ const agentId = agent.agent_id;
4590
+ api.post("/host/integration-health", {
4591
+ agent_id: agentId,
4592
+ toolkit_id: toolkitId,
4593
+ action: "mark_unhealthy",
4594
+ reason: `mcp-presence-reaper cap (${3} consecutive failed restart cycles) tripped on '${serverKey}'`
4595
+ }).then((res) => {
4596
+ log(
4597
+ `[mcp-presence-reaper] marked integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${JSON.stringify(res).slice(0, 200)}`
4598
+ );
4599
+ }).catch((err) => {
4600
+ log(
4601
+ `[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`
4602
+ );
4603
+ });
4604
+ },
4605
+ // ENG-5286: record the restart event with the API so the
4606
+ // agent-heartbeat-monitor cron can aggregate per-agent counts
4607
+ // into the McpReaperRestartsHourly CloudWatch metric. Fire-and-
4608
+ // forget — the reaper's correctness must not depend on this.
4609
+ onRestart: (codeName, { activeKeys, givenUpKeys }) => {
4610
+ api.post("/host/mcp-reaper-restart", {
4611
+ agent_id: agent.agent_id,
4612
+ code_name: codeName,
4613
+ server_keys: activeKeys,
4614
+ given_up_keys: givenUpKeys
4615
+ }).catch((err) => {
4616
+ log(
4617
+ `[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`
4618
+ );
4619
+ });
4620
+ }
4366
4621
  });
4367
4622
  } catch (err) {
4368
4623
  log(`[mcp-presence-reaper] presence check failed for '${agent.code_name}': ${err.message}`);