@integrity-labs/agt-cli 0.27.116 → 0.27.118

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 CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  success,
29
29
  table,
30
30
  warn
31
- } from "../chunk-7UIEHD5Q.js";
31
+ } from "../chunk-WQV2432Z.js";
32
32
  import {
33
33
  CHANNEL_REGISTRY,
34
34
  DEPLOYMENT_TEMPLATES,
@@ -4934,7 +4934,7 @@ import { execFileSync, execSync } from "child_process";
4934
4934
  import { existsSync as existsSync10, realpathSync as realpathSync2 } from "fs";
4935
4935
  import chalk18 from "chalk";
4936
4936
  import ora16 from "ora";
4937
- var cliVersion = true ? "0.27.116" : "dev";
4937
+ var cliVersion = true ? "0.27.118" : "dev";
4938
4938
  async function fetchLatestVersion() {
4939
4939
  const host2 = getHost();
4940
4940
  if (!host2) return null;
@@ -5857,7 +5857,7 @@ function handleError(err) {
5857
5857
  }
5858
5858
 
5859
5859
  // src/bin/agt.ts
5860
- var cliVersion2 = true ? "0.27.116" : "dev";
5860
+ var cliVersion2 = true ? "0.27.118" : "dev";
5861
5861
  var program = new Command();
5862
5862
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
5863
5863
  program.hook("preAction", async (thisCommand, actionCommand) => {
@@ -7603,4 +7603,4 @@ export {
7603
7603
  managerInstallSystemUnitCommand,
7604
7604
  managerUninstallSystemUnitCommand
7605
7605
  };
7606
- //# sourceMappingURL=chunk-7UIEHD5Q.js.map
7606
+ //# sourceMappingURL=chunk-WQV2432Z.js.map
@@ -17,7 +17,7 @@ import {
17
17
  provisionStopHook,
18
18
  requireHost,
19
19
  safeWriteJsonAtomic
20
- } from "../chunk-7UIEHD5Q.js";
20
+ } from "../chunk-WQV2432Z.js";
21
21
  import {
22
22
  getProjectDir as getProjectDir2,
23
23
  getReadyTasks,
@@ -971,6 +971,99 @@ function formatStatusMessage(events, windowMs) {
971
971
  return `Circuit breaker tripped: ${events.length} restarts in ${windowLabel} (${breakdown}); most recent=${last.reason} at ${new Date(last.at).toISOString()}`;
972
972
  }
973
973
 
974
+ // src/lib/mcp-flap-dampener.ts
975
+ var DEFAULT_WINDOW_MS2 = 6e5;
976
+ var DEFAULT_MIN_TRANSITIONS = 4;
977
+ var DEFAULT_MIN_DISTINCT = 2;
978
+ function readEnvNumber2(name, fallback) {
979
+ const raw = process.env[name];
980
+ if (!raw) return fallback;
981
+ const parsed = Number(raw);
982
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
983
+ }
984
+ function membershipSignature(keys) {
985
+ return [...new Set(keys)].sort().join(",");
986
+ }
987
+ var McpFlapDampener = class {
988
+ windowMs;
989
+ minTransitions;
990
+ minDistinct;
991
+ now;
992
+ history = /* @__PURE__ */ new Map();
993
+ flapping = /* @__PURE__ */ new Set();
994
+ constructor(opts = {}) {
995
+ this.windowMs = opts.windowMs ?? readEnvNumber2("AGT_MCP_FLAP_WINDOW_MS", DEFAULT_WINDOW_MS2);
996
+ this.minTransitions = opts.minTransitions ?? readEnvNumber2("AGT_MCP_FLAP_MIN_TRANSITIONS", DEFAULT_MIN_TRANSITIONS);
997
+ this.minDistinct = opts.minDistinct ?? readEnvNumber2("AGT_MCP_FLAP_MIN_DISTINCT", DEFAULT_MIN_DISTINCT);
998
+ this.now = opts.now ?? Date.now;
999
+ if (!Number.isFinite(this.windowMs) || this.windowMs < 1e3) {
1000
+ throw new Error("mcp-flap-dampener windowMs must be a finite number >= 1000");
1001
+ }
1002
+ if (!Number.isFinite(this.minTransitions) || this.minTransitions < 2) {
1003
+ throw new Error("mcp-flap-dampener minTransitions must be a finite number >= 2");
1004
+ }
1005
+ if (!Number.isFinite(this.minDistinct) || this.minDistinct < 2) {
1006
+ throw new Error("mcp-flap-dampener minDistinct must be a finite number >= 2");
1007
+ }
1008
+ }
1009
+ key(codeName, channel) {
1010
+ return `${codeName}\0${channel}`;
1011
+ }
1012
+ /**
1013
+ * Record an observed membership signature for (codeName, channel) and return
1014
+ * the current flap decision. Call this on every poll where the manager has
1015
+ * computed the desired set — including unchanged polls — so a set that has
1016
+ * gone quiet ages out of the window and the channel recovers.
1017
+ */
1018
+ record(codeName, channel, signature) {
1019
+ const k = this.key(codeName, channel);
1020
+ const at = this.now();
1021
+ const cutoff = at - this.windowMs;
1022
+ const prior = (this.history.get(k) ?? []).filter((o) => o.at >= cutoff);
1023
+ prior.push({ sig: signature, at });
1024
+ this.history.set(k, prior);
1025
+ let transitions = 0;
1026
+ for (let i = 1; i < prior.length; i++) {
1027
+ if (prior[i].sig !== prior[i - 1].sig) transitions++;
1028
+ }
1029
+ const distinctStates = new Set(prior.map((o) => o.sig)).size;
1030
+ const revisited = prior.slice(0, -1).some((o, i) => o.sig === signature && i !== prior.length - 2);
1031
+ const transitionNow = prior.length >= 2 && prior[prior.length - 1].sig !== prior[prior.length - 2].sig;
1032
+ const oscillating = transitions >= this.minTransitions && distinctStates >= this.minDistinct && revisited;
1033
+ const wasFlapping = this.flapping.has(k);
1034
+ let onset = false;
1035
+ if (wasFlapping) {
1036
+ if (!transitionNow) this.flapping.delete(k);
1037
+ } else if (transitionNow && oscillating) {
1038
+ this.flapping.add(k);
1039
+ onset = true;
1040
+ }
1041
+ return {
1042
+ flapping: this.flapping.has(k),
1043
+ distinctStates,
1044
+ transitions,
1045
+ windowCount: prior.length,
1046
+ onset
1047
+ };
1048
+ }
1049
+ /** True if (codeName, channel) is currently dampened. */
1050
+ isFlapping(codeName, channel) {
1051
+ return this.flapping.has(this.key(codeName, channel));
1052
+ }
1053
+ /** Drop all state for an agent (called on deprovision / agent removal). */
1054
+ forget(codeName) {
1055
+ const prefix = `${codeName}\0`;
1056
+ for (const k of [...this.history.keys()]) {
1057
+ if (k.startsWith(prefix)) this.history.delete(k);
1058
+ }
1059
+ for (const k of [...this.flapping]) {
1060
+ if (k.startsWith(prefix)) this.flapping.delete(k);
1061
+ }
1062
+ }
1063
+ };
1064
+ var FLAP_CHANNEL_INTEGRATIONS = "integrations";
1065
+ var FLAP_CHANNEL_MANAGED_MCP = "managed-mcp";
1066
+
974
1067
  // src/lib/auto-resume.ts
975
1068
  var DEFAULT_QUIET_MS = 18e5;
976
1069
  var DEFAULT_BACKOFF_RESET_MS = 864e5;
@@ -3803,6 +3896,19 @@ function hasRevokedResiduals(state6) {
3803
3896
  var pendingSessionRestarts = /* @__PURE__ */ new Map();
3804
3897
  var restartBreaker = new RestartBreaker();
3805
3898
  var reportedTrips = /* @__PURE__ */ new Map();
3899
+ var mcpFlapDampener = new McpFlapDampener();
3900
+ function recordConfigChurnEvent(agentId, codeName, channel, signature) {
3901
+ api.post("/host/mcp-config-churn", {
3902
+ agent_id: agentId,
3903
+ code_name: codeName,
3904
+ channel,
3905
+ signature
3906
+ }).catch((err) => {
3907
+ log(
3908
+ `[mcp-flap-dampener] failed to record config-churn event for '${codeName}' (${channel}) (ENG-6123): ${err.message} \u2014 local suppression still active; CloudWatch metric will under-count`
3909
+ );
3910
+ });
3911
+ }
3806
3912
  function recordRestartForBreaker(codeName, reason) {
3807
3913
  const result = restartBreaker.record(codeName, reason);
3808
3914
  if (!result.tripped || !result.trip) return;
@@ -3901,12 +4007,17 @@ function scheduleSessionRestart(codeName, delayMs, reason, breakerReason = "hot-
3901
4007
  pendingSessionRestarts.delete(codeName);
3902
4008
  const gate = restartGateFor(codeName, breakerReason);
3903
4009
  if (gate !== "bypass" && gate !== "proceed") {
3904
- log(
3905
- `[maintenance-window] Deferring '${reason}' restart for '${codeName}' (${gate}) \u2014 re-checking in ${RESTART_DEFER_RECHECK_MS / 1e3}s`
3906
- );
4010
+ const lastLogged = deferLogThrottle.get(codeName);
4011
+ if (lastLogged === void 0 || Date.now() - lastLogged >= DEFER_LOG_THROTTLE_MS) {
4012
+ log(
4013
+ `[maintenance-window] Deferring '${reason}' restart for '${codeName}' (${gate}) \u2014 re-checking every ${RESTART_DEFER_RECHECK_MS / 1e3}s until the window opens`
4014
+ );
4015
+ deferLogThrottle.set(codeName, Date.now());
4016
+ }
3907
4017
  scheduleSessionRestart(codeName, RESTART_DEFER_RECHECK_MS, reason, breakerReason);
3908
4018
  return;
3909
4019
  }
4020
+ deferLogThrottle.delete(codeName);
3910
4021
  stopPersistentSession(codeName, log);
3911
4022
  runningMcpHashes.delete(codeName);
3912
4023
  recordRestartForBreaker(codeName, breakerReason);
@@ -3920,9 +4031,12 @@ function cancelPendingSessionRestart(codeName) {
3920
4031
  if (!existing) return;
3921
4032
  clearTimeout(existing);
3922
4033
  pendingSessionRestarts.delete(codeName);
4034
+ deferLogThrottle.delete(codeName);
3923
4035
  log(`[hot-reload] Cancelled pending restart timer for '${codeName}' (another teardown path is handling it)`);
3924
4036
  }
3925
4037
  var RESTART_DEFER_RECHECK_MS = 6e4;
4038
+ var DEFER_LOG_THROTTLE_MS = 6e5;
4039
+ var deferLogThrottle = /* @__PURE__ */ new Map();
3926
4040
  var lastInboundMs = /* @__PURE__ */ new Map();
3927
4041
  var consecutiveWedgeCycles = /* @__PURE__ */ new Map();
3928
4042
  function noteInbound(codeName) {
@@ -4099,6 +4213,7 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
4099
4213
  const removed = decision.addedOrRemoved.filter((k) => !currentKeys.has(k));
4100
4214
  quarantineOnlyDrift = added.length === 0 && removed.length > 0 && removed.every((k) => quarantined.has(k));
4101
4215
  }
4216
+ const flapSuppressed = decision.restart && !quarantineOnlyDrift && mcpFlapDampener.isFlapping(codeName, FLAP_CHANNEL_MANAGED_MCP);
4102
4217
  if (decision.membershipUnknown) {
4103
4218
  clearPresenceReaperState(codeName);
4104
4219
  log(
@@ -4108,6 +4223,10 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
4108
4223
  log(
4109
4224
  `[channel-quarantine] .mcp.json drift for '${codeName}' is quarantined-channel removal only [${decision.addedOrRemoved.join(", ")}] \u2014 adopting new baseline WITHOUT restart (0-restart-on-removal, ENG-5932)`
4110
4225
  );
4226
+ } else if (flapSuppressed) {
4227
+ log(
4228
+ `[mcp-flap-dampener] .mcp.json membership drift for '${codeName}' [${decision.addedOrRemoved.join(", ")}] held \u2014 managed-MCP set is flapping, adopting baseline WITHOUT restart until it settles (ENG-6123)`
4229
+ );
4111
4230
  } else if (decision.restart) {
4112
4231
  clearPresenceReaperStateForKeys(codeName, new Set(decision.addedOrRemoved));
4113
4232
  log(
@@ -4118,10 +4237,11 @@ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
4118
4237
  `[hot-reload] .mcp.json value-only drift for '${codeName}' \u2014 preserving presence-reaper state and NOT restarting (ENG-5285/ENG-5537)`
4119
4238
  );
4120
4239
  }
4121
- if (decision.restart && !quarantineOnlyDrift) {
4240
+ if (decision.restart && !quarantineOnlyDrift && !flapSuppressed) {
4122
4241
  scheduleSessionRestart(codeName, 0, ".mcp.json content change (ENG-4897)");
4123
4242
  runningMcpHashes.delete(codeName);
4124
4243
  runningMcpServerKeys.delete(codeName);
4244
+ } else if (flapSuppressed) {
4125
4245
  } else {
4126
4246
  runningMcpHashes.set(codeName, action.current);
4127
4247
  if (currentKeys) runningMcpServerKeys.set(codeName, currentKeys);
@@ -4216,6 +4336,8 @@ function clearAgentCaches(agentId, codeName) {
4216
4336
  for (const key of taskDisplayInfo.keys()) {
4217
4337
  if (key.startsWith(`${codeName}:`)) taskDisplayInfo.delete(key);
4218
4338
  }
4339
+ mcpFlapDampener.forget(codeName);
4340
+ deferLogThrottle.delete(codeName);
4219
4341
  if (channelCacheMutated) saveChannelHashCache2();
4220
4342
  }
4221
4343
  var cachedFrameworkVersion = null;
@@ -4223,7 +4345,7 @@ var cachedMaintenanceWindow = null;
4223
4345
  var lastVersionCheckAt = 0;
4224
4346
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
4225
4347
  var lastResponsivenessProbeAt = 0;
4226
- var agtCliVersion = true ? "0.27.116" : "dev";
4348
+ var agtCliVersion = true ? "0.27.118" : "dev";
4227
4349
  function resolveBrewPath(execFileSync4) {
4228
4350
  try {
4229
4351
  const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -5503,7 +5625,7 @@ async function pollCycle() {
5503
5625
  const {
5504
5626
  collectResponsivenessProbes,
5505
5627
  getResponsivenessIntervalMs
5506
- } = await import("../responsiveness-probe-FLQZ7E5H.js");
5628
+ } = await import("../responsiveness-probe-EQG6WWSN.js");
5507
5629
  const probeIntervalMs = getResponsivenessIntervalMs();
5508
5630
  if (now - lastResponsivenessProbeAt > probeIntervalMs) {
5509
5631
  const probeCodeNames = [...agentState.persistentSessionAgents];
@@ -5538,7 +5660,12 @@ async function pollCycle() {
5538
5660
  try {
5539
5661
  const wedgeConfig = resolveWedgeConfig();
5540
5662
  if (wedgeConfig.mode !== "off") {
5541
- const { collectResponsivenessProbes } = await import("../responsiveness-probe-FLQZ7E5H.js");
5663
+ const {
5664
+ collectResponsivenessProbes,
5665
+ livePendingInboundOldestAgeSeconds,
5666
+ deadLetterPendingInbound
5667
+ } = await import("../responsiveness-probe-EQG6WWSN.js");
5668
+ const wedgeNow = /* @__PURE__ */ new Date();
5542
5669
  const liveAgents = agentState.persistentSessionAgents;
5543
5670
  for (const tracked of consecutiveWedgeCycles.keys()) {
5544
5671
  if (!liveAgents.has(tracked)) consecutiveWedgeCycles.delete(tracked);
@@ -5549,9 +5676,14 @@ async function pollCycle() {
5549
5676
  consecutiveWedgeCycles.delete(codeName);
5550
5677
  continue;
5551
5678
  }
5679
+ const sessionStartMs = getSessionState(codeName)?.startedAt ?? null;
5552
5680
  const signals = {
5553
5681
  paneActivityAgeSeconds: probe.pane_activity_age_seconds,
5554
- pendingInboundOldestAgeSeconds: probe.pending_inbound_oldest_age_seconds ?? null
5682
+ pendingInboundOldestAgeSeconds: livePendingInboundOldestAgeSeconds(
5683
+ codeName,
5684
+ sessionStartMs,
5685
+ wedgeNow
5686
+ )
5555
5687
  };
5556
5688
  if (!isWedgeCandidateCycle(signals, wedgeConfig)) {
5557
5689
  consecutiveWedgeCycles.delete(codeName);
@@ -5578,7 +5710,11 @@ async function pollCycle() {
5578
5710
  }
5579
5711
  stopPersistentSessionAndForgetMcpBaseline(codeName);
5580
5712
  consecutiveWedgeCycles.delete(codeName);
5581
- log(`[wedge] forced fresh respawn ${detail} \u2192 new session ${newId} (transcript preserved)`);
5713
+ const deadLettered = deadLetterPendingInbound(codeName, wedgeNow);
5714
+ const deadNote = deadLettered > 0 ? `, ${deadLettered} stale inbound dead-lettered` : "";
5715
+ log(
5716
+ `[wedge] forced fresh respawn ${detail} \u2192 new session ${newId} (transcript preserved${deadNote})`
5717
+ );
5582
5718
  } catch (err) {
5583
5719
  log(`[wedge] force-fresh respawn failed for ${codeName}: ${err.message}`);
5584
5720
  }
@@ -6762,6 +6898,14 @@ async function processAgent(agent, agentStates) {
6762
6898
  if (integrations.length > 0) {
6763
6899
  const intHash = computeIntegrationsHash(integrations);
6764
6900
  const prevIntHash = agentState.knownIntegrationHashes.get(agent.agent_id);
6901
+ const intMembership = membershipSignature(integrations.map((i) => i.definition_id));
6902
+ const intFlap = mcpFlapDampener.record(agent.code_name, FLAP_CHANNEL_INTEGRATIONS, intMembership);
6903
+ if (intFlap.onset) {
6904
+ log(
6905
+ `[mcp-flap-dampener] integration set for '${agent.code_name}' is FLAPPING (${intFlap.transitions} transitions / ${intFlap.distinctStates} states in window) \u2014 suppressing integration-update notices until it settles (ENG-6123)`
6906
+ );
6907
+ recordConfigChurnEvent(agent.agent_id, agent.code_name, FLAP_CHANNEL_INTEGRATIONS, intMembership);
6908
+ }
6765
6909
  if (intHash !== prevIntHash) {
6766
6910
  const projectDir = join8(homedir4(), ".augmented", agent.code_name, "project");
6767
6911
  const envIntPath = join8(projectDir, ".env.integrations");
@@ -6795,11 +6939,15 @@ async function processAgent(agent, agentStates) {
6795
6939
  }
6796
6940
  const names = integrations.map((i) => i.display_name || i.definition_id).join(", ");
6797
6941
  const reapNote = affectedServerKeys.length > 0 ? ` The MCP servers that depend on rotating credentials (${affectedServerKeys.join(", ")}) have been signalled to reconnect.` : "";
6798
- injectMessage(agent.code_name, "system", `Your integrations have been refreshed. You have access to: ${names}.${reapNote}`, {
6799
- task_name: "integration-update"
6800
- }, log).catch(() => {
6801
- });
6802
- log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names} (reaped ${affectedServerKeys.length} stale MCP server(s))`);
6942
+ if (intFlap.flapping) {
6943
+ log(`[mcp-flap-dampener] suppressed integration-update notice for '${agent.code_name}' (set flapping, ENG-6123): ${names}`);
6944
+ } else {
6945
+ injectMessage(agent.code_name, "system", `Your integrations have been refreshed. You have access to: ${names}.${reapNote}`, {
6946
+ task_name: "integration-update"
6947
+ }, log).catch(() => {
6948
+ });
6949
+ log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names} (reaped ${affectedServerKeys.length} stale MCP server(s))`);
6950
+ }
6803
6951
  } catch (err) {
6804
6952
  rotationHandled = false;
6805
6953
  log(`[hot-reload] Failed to compute / reap affected MCP servers for '${agent.code_name}': ${err.message} \u2014 will retry next tick`);
@@ -6839,6 +6987,14 @@ async function processAgent(agent, agentStates) {
6839
6987
  const prevMcpHash = agentState.knownManagedMcpHashes.get(agent.agent_id);
6840
6988
  const structureHash = managedMcpStructureHash(desiredEntries);
6841
6989
  const prevStructureHash = agentState.knownManagedMcpStructure.get(agent.agent_id);
6990
+ const mcpMembership = membershipSignature(expectedServerIds);
6991
+ const mcpFlap = mcpFlapDampener.record(agent.code_name, FLAP_CHANNEL_MANAGED_MCP, mcpMembership);
6992
+ if (mcpFlap.onset) {
6993
+ log(
6994
+ `[mcp-flap-dampener] managed-MCP set for '${agent.code_name}' is FLAPPING (${mcpFlap.transitions} transitions / ${mcpFlap.distinctStates} states in window) \u2014 suppressing mcp-update notice + restart until it settles (ENG-6123)`
6995
+ );
6996
+ recordConfigChurnEvent(agent.agent_id, agent.code_name, FLAP_CHANNEL_MANAGED_MCP, mcpMembership);
6997
+ }
6842
6998
  if (mcpHash !== prevMcpHash) {
6843
6999
  for (const e of desiredEntries) {
6844
7000
  frameworkAdapter.writeMcpServer(agent.code_name, e.serverId, { url: e.url, headers: e.headers });
@@ -6877,7 +7033,12 @@ async function processAgent(agent, agentStates) {
6877
7033
  }
6878
7034
  agentState.knownManagedMcpHashes.set(agent.agent_id, mcpHash);
6879
7035
  agentState.knownManagedMcpStructure.set(agent.agent_id, structureHash);
6880
- if (prevStructureHash !== void 0 && structureHash !== prevStructureHash && fwForMcp === "claude-code" && isSessionHealthy(agent.code_name)) {
7036
+ if (prevStructureHash !== void 0 && structureHash !== prevStructureHash && fwForMcp === "claude-code" && isSessionHealthy(agent.code_name) && // ENG-6123: hold the restart + inject-notice while the managed-MCP
7037
+ // membership is oscillating. The .mcp.json above was still written
7038
+ // (the file converges to the live set); we only withhold the
7039
+ // opus-facing notice + the session reload — the two per-flip burns.
7040
+ // When the set settles, the next structural change restarts once.
7041
+ !mcpFlap.flapping) {
6881
7042
  const mcpNames = agentToolkits.map((tk) => tk.toolkit_name).join(", ") || "none (all removed)";
6882
7043
  log(`[hot-reload] MCP servers changed for '${agent.code_name}': ${mcpNames} \u2014 restarting session`);
6883
7044
  const restartNotice = agentToolkits.length > 0 ? `New MCP tool servers have been configured: ${mcpNames}. Note: MCP servers require a session restart to connect. Your manager will restart your session shortly.` : "Managed MCP tool servers were removed. Your manager will restart your session shortly so the session drops those tools.";