@integrity-labs/agt-cli 0.20.8 → 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.
@@ -22,7 +22,7 @@ import {
22
22
  resolveChannels,
23
23
  resolveDmTarget,
24
24
  wrapScheduledTaskPrompt
25
- } from "../chunk-5A5EBN5L.js";
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) [${missing.join(", ")}] have no live children \u2014 restarting session`
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
- case "drift":
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.20.8" : "dev";
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();
@@ -3765,13 +3903,15 @@ async function processAgent(agent, agentStates) {
3765
3903
  crossTeamPeerIntraOrg: crossTeamData.organization?.cross_team_peer_intra_org ?? "unrestricted"
3766
3904
  } : void 0;
3767
3905
  const peersForHash = channelId === "telegram" ? extractCharterTelegramPeers(refreshData.charter?.raw_content ?? "", gateContext) : channelId === "slack" ? extractCharterSlackPeers(refreshData.charter?.raw_content ?? "", gateContext) : null;
3768
- const CHANNEL_WRITE_VERSION = 3;
3906
+ const sessionModeForHash = refreshData.agent.session_mode;
3907
+ const CHANNEL_WRITE_VERSION = 4;
3769
3908
  const configHash = createHash2("sha256").update(
3770
3909
  canonicalJson({
3771
3910
  writeVersion: CHANNEL_WRITE_VERSION,
3772
3911
  config: entry.config,
3773
3912
  team: teamSettingsForHash,
3774
- peers: peersForHash
3913
+ peers: peersForHash,
3914
+ sessionMode: sessionModeForHash ?? null
3775
3915
  })
3776
3916
  ).digest("hex");
3777
3917
  const cacheKey = `${agent.agent_id}:${channelId}`;
@@ -4009,6 +4149,10 @@ async function processAgent(agent, agentStates) {
4009
4149
  for (const tk of agentToolkits) {
4010
4150
  const serverId = tk.toolkit_id.replace(/[^a-z0-9]/gi, "_").toLowerCase();
4011
4151
  expectedServerIds.add(serverId);
4152
+ managedToolkitIdByAgentAndServerId.set(
4153
+ managedToolkitMapKey(agent.agent_id, serverId),
4154
+ tk.toolkit_id
4155
+ );
4012
4156
  const mcpUrl = tk.mcp_url;
4013
4157
  const mcpHeaders = tk.mcp_headers;
4014
4158
  const proxyUrl = tk.proxy_url.startsWith("/") ? `${requireHost()}${tk.proxy_url}` : tk.proxy_url;
@@ -4360,7 +4504,60 @@ async function processAgent(agent, agentStates) {
4360
4504
  codeName: agent.code_name,
4361
4505
  mcpJson: mcpJsonParsed,
4362
4506
  sessionStartedAt: sess?.startedAt ?? null,
4363
- 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
+ }
4364
4561
  });
4365
4562
  } catch (err) {
4366
4563
  log(`[mcp-presence-reaper] presence check failed for '${agent.code_name}': ${err.message}`);