@integrity-labs/agt-cli 0.27.116 → 0.27.117
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-
|
|
31
|
+
} from "../chunk-SDRBYSRO.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.
|
|
4937
|
+
var cliVersion = true ? "0.27.117" : "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.
|
|
5860
|
+
var cliVersion2 = true ? "0.27.117" : "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) => {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
provisionStopHook,
|
|
18
18
|
requireHost,
|
|
19
19
|
safeWriteJsonAtomic
|
|
20
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-SDRBYSRO.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
|
-
|
|
3905
|
-
|
|
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.
|
|
4348
|
+
var agtCliVersion = true ? "0.27.117" : "dev";
|
|
4227
4349
|
function resolveBrewPath(execFileSync4) {
|
|
4228
4350
|
try {
|
|
4229
4351
|
const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
@@ -6762,6 +6884,14 @@ async function processAgent(agent, agentStates) {
|
|
|
6762
6884
|
if (integrations.length > 0) {
|
|
6763
6885
|
const intHash = computeIntegrationsHash(integrations);
|
|
6764
6886
|
const prevIntHash = agentState.knownIntegrationHashes.get(agent.agent_id);
|
|
6887
|
+
const intMembership = membershipSignature(integrations.map((i) => i.definition_id));
|
|
6888
|
+
const intFlap = mcpFlapDampener.record(agent.code_name, FLAP_CHANNEL_INTEGRATIONS, intMembership);
|
|
6889
|
+
if (intFlap.onset) {
|
|
6890
|
+
log(
|
|
6891
|
+
`[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)`
|
|
6892
|
+
);
|
|
6893
|
+
recordConfigChurnEvent(agent.agent_id, agent.code_name, FLAP_CHANNEL_INTEGRATIONS, intMembership);
|
|
6894
|
+
}
|
|
6765
6895
|
if (intHash !== prevIntHash) {
|
|
6766
6896
|
const projectDir = join8(homedir4(), ".augmented", agent.code_name, "project");
|
|
6767
6897
|
const envIntPath = join8(projectDir, ".env.integrations");
|
|
@@ -6795,11 +6925,15 @@ async function processAgent(agent, agentStates) {
|
|
|
6795
6925
|
}
|
|
6796
6926
|
const names = integrations.map((i) => i.display_name || i.definition_id).join(", ");
|
|
6797
6927
|
const reapNote = affectedServerKeys.length > 0 ? ` The MCP servers that depend on rotating credentials (${affectedServerKeys.join(", ")}) have been signalled to reconnect.` : "";
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
}
|
|
6801
|
-
|
|
6802
|
-
|
|
6928
|
+
if (intFlap.flapping) {
|
|
6929
|
+
log(`[mcp-flap-dampener] suppressed integration-update notice for '${agent.code_name}' (set flapping, ENG-6123): ${names}`);
|
|
6930
|
+
} else {
|
|
6931
|
+
injectMessage(agent.code_name, "system", `Your integrations have been refreshed. You have access to: ${names}.${reapNote}`, {
|
|
6932
|
+
task_name: "integration-update"
|
|
6933
|
+
}, log).catch(() => {
|
|
6934
|
+
});
|
|
6935
|
+
log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names} (reaped ${affectedServerKeys.length} stale MCP server(s))`);
|
|
6936
|
+
}
|
|
6803
6937
|
} catch (err) {
|
|
6804
6938
|
rotationHandled = false;
|
|
6805
6939
|
log(`[hot-reload] Failed to compute / reap affected MCP servers for '${agent.code_name}': ${err.message} \u2014 will retry next tick`);
|
|
@@ -6839,6 +6973,14 @@ async function processAgent(agent, agentStates) {
|
|
|
6839
6973
|
const prevMcpHash = agentState.knownManagedMcpHashes.get(agent.agent_id);
|
|
6840
6974
|
const structureHash = managedMcpStructureHash(desiredEntries);
|
|
6841
6975
|
const prevStructureHash = agentState.knownManagedMcpStructure.get(agent.agent_id);
|
|
6976
|
+
const mcpMembership = membershipSignature(expectedServerIds);
|
|
6977
|
+
const mcpFlap = mcpFlapDampener.record(agent.code_name, FLAP_CHANNEL_MANAGED_MCP, mcpMembership);
|
|
6978
|
+
if (mcpFlap.onset) {
|
|
6979
|
+
log(
|
|
6980
|
+
`[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)`
|
|
6981
|
+
);
|
|
6982
|
+
recordConfigChurnEvent(agent.agent_id, agent.code_name, FLAP_CHANNEL_MANAGED_MCP, mcpMembership);
|
|
6983
|
+
}
|
|
6842
6984
|
if (mcpHash !== prevMcpHash) {
|
|
6843
6985
|
for (const e of desiredEntries) {
|
|
6844
6986
|
frameworkAdapter.writeMcpServer(agent.code_name, e.serverId, { url: e.url, headers: e.headers });
|
|
@@ -6877,7 +7019,12 @@ async function processAgent(agent, agentStates) {
|
|
|
6877
7019
|
}
|
|
6878
7020
|
agentState.knownManagedMcpHashes.set(agent.agent_id, mcpHash);
|
|
6879
7021
|
agentState.knownManagedMcpStructure.set(agent.agent_id, structureHash);
|
|
6880
|
-
if (prevStructureHash !== void 0 && structureHash !== prevStructureHash && fwForMcp === "claude-code" && isSessionHealthy(agent.code_name)
|
|
7022
|
+
if (prevStructureHash !== void 0 && structureHash !== prevStructureHash && fwForMcp === "claude-code" && isSessionHealthy(agent.code_name) && // ENG-6123: hold the restart + inject-notice while the managed-MCP
|
|
7023
|
+
// membership is oscillating. The .mcp.json above was still written
|
|
7024
|
+
// (the file converges to the live set); we only withhold the
|
|
7025
|
+
// opus-facing notice + the session reload — the two per-flip burns.
|
|
7026
|
+
// When the set settles, the next structural change restarts once.
|
|
7027
|
+
!mcpFlap.flapping) {
|
|
6881
7028
|
const mcpNames = agentToolkits.map((tk) => tk.toolkit_name).join(", ") || "none (all removed)";
|
|
6882
7029
|
log(`[hot-reload] MCP servers changed for '${agent.code_name}': ${mcpNames} \u2014 restarting session`);
|
|
6883
7030
|
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.";
|