@integrity-labs/agt-cli 0.27.115 → 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 +3 -3
- package/dist/{chunk-EOMWSV4B.js → chunk-SDRBYSRO.js} +15 -3
- package/dist/{chunk-EOMWSV4B.js.map → chunk-SDRBYSRO.js.map} +1 -1
- package/dist/lib/manager-worker.js +162 -13
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/slack-channel.js +118 -36
- package/package.json +1 -1
|
@@ -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();
|
|
@@ -6441,6 +6563,7 @@ async function processAgent(agent, agentStates) {
|
|
|
6441
6563
|
const telegramPeers = channelId === "telegram" ? extractCharterTelegramPeers(refreshData.charter?.raw_content ?? "", gateContext) : void 0;
|
|
6442
6564
|
const slackPeers = channelId === "slack" ? extractCharterSlackPeers(refreshData.charter?.raw_content ?? "", gateContext) : void 0;
|
|
6443
6565
|
const senderPolicyForCall = channelId === "slack" || channelId === "msteams" ? refreshData.sender_policy ?? void 0 : void 0;
|
|
6566
|
+
const agentAvatarUrl = channelId === "slack" ? (refreshData.agent.avatar_url ?? void 0) || void 0 : void 0;
|
|
6444
6567
|
frameworkAdapter.writeChannelCredentials(
|
|
6445
6568
|
agent.code_name,
|
|
6446
6569
|
channelId,
|
|
@@ -6453,7 +6576,8 @@ async function processAgent(agent, agentStates) {
|
|
|
6453
6576
|
telegramPeers,
|
|
6454
6577
|
slackPeers,
|
|
6455
6578
|
agentTimezone,
|
|
6456
|
-
senderPolicy: senderPolicyForCall
|
|
6579
|
+
senderPolicy: senderPolicyForCall,
|
|
6580
|
+
agentAvatarUrl
|
|
6457
6581
|
}
|
|
6458
6582
|
);
|
|
6459
6583
|
agentState.knownChannelConfigHashes.set(cacheKey, configHash);
|
|
@@ -6760,6 +6884,14 @@ async function processAgent(agent, agentStates) {
|
|
|
6760
6884
|
if (integrations.length > 0) {
|
|
6761
6885
|
const intHash = computeIntegrationsHash(integrations);
|
|
6762
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
|
+
}
|
|
6763
6895
|
if (intHash !== prevIntHash) {
|
|
6764
6896
|
const projectDir = join8(homedir4(), ".augmented", agent.code_name, "project");
|
|
6765
6897
|
const envIntPath = join8(projectDir, ".env.integrations");
|
|
@@ -6793,11 +6925,15 @@ async function processAgent(agent, agentStates) {
|
|
|
6793
6925
|
}
|
|
6794
6926
|
const names = integrations.map((i) => i.display_name || i.definition_id).join(", ");
|
|
6795
6927
|
const reapNote = affectedServerKeys.length > 0 ? ` The MCP servers that depend on rotating credentials (${affectedServerKeys.join(", ")}) have been signalled to reconnect.` : "";
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
}
|
|
6799
|
-
|
|
6800
|
-
|
|
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
|
+
}
|
|
6801
6937
|
} catch (err) {
|
|
6802
6938
|
rotationHandled = false;
|
|
6803
6939
|
log(`[hot-reload] Failed to compute / reap affected MCP servers for '${agent.code_name}': ${err.message} \u2014 will retry next tick`);
|
|
@@ -6837,6 +6973,14 @@ async function processAgent(agent, agentStates) {
|
|
|
6837
6973
|
const prevMcpHash = agentState.knownManagedMcpHashes.get(agent.agent_id);
|
|
6838
6974
|
const structureHash = managedMcpStructureHash(desiredEntries);
|
|
6839
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
|
+
}
|
|
6840
6984
|
if (mcpHash !== prevMcpHash) {
|
|
6841
6985
|
for (const e of desiredEntries) {
|
|
6842
6986
|
frameworkAdapter.writeMcpServer(agent.code_name, e.serverId, { url: e.url, headers: e.headers });
|
|
@@ -6875,7 +7019,12 @@ async function processAgent(agent, agentStates) {
|
|
|
6875
7019
|
}
|
|
6876
7020
|
agentState.knownManagedMcpHashes.set(agent.agent_id, mcpHash);
|
|
6877
7021
|
agentState.knownManagedMcpStructure.set(agent.agent_id, structureHash);
|
|
6878
|
-
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) {
|
|
6879
7028
|
const mcpNames = agentToolkits.map((tk) => tk.toolkit_name).join(", ") || "none (all removed)";
|
|
6880
7029
|
log(`[hot-reload] MCP servers changed for '${agent.code_name}': ${mcpNames} \u2014 restarting session`);
|
|
6881
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.";
|