@integrity-labs/agt-cli 0.28.341 → 0.28.343
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-I3YS5WFV.js → chunk-EP6E6CIY.js} +3 -3
- package/dist/chunk-EP6E6CIY.js.map +1 -0
- package/dist/{chunk-SRRZO75K.js → chunk-GAYSFNTX.js} +13 -4
- package/dist/{chunk-SRRZO75K.js.map → chunk-GAYSFNTX.js.map} +1 -1
- package/dist/lib/manager-worker.js +371 -181
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/{claude-scheduler-FATCLHDM.js → scheduler-engine-HYOOQV4A.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-I3YS5WFV.js.map +0 -1
- /package/dist/{claude-scheduler-FATCLHDM.js.map → scheduler-engine-HYOOQV4A.js.map} +0 -0
|
@@ -46,14 +46,14 @@ import {
|
|
|
46
46
|
resolveEffectivePinRaw,
|
|
47
47
|
safeWriteJsonAtomic,
|
|
48
48
|
setConfigHash
|
|
49
|
-
} from "../chunk-
|
|
49
|
+
} from "../chunk-GAYSFNTX.js";
|
|
50
50
|
import {
|
|
51
51
|
getProjectDir as getProjectDir2,
|
|
52
52
|
getReadyTasks,
|
|
53
53
|
loadSchedulerState,
|
|
54
54
|
markTaskFired,
|
|
55
55
|
syncTasksToScheduler
|
|
56
|
-
} from "../chunk-
|
|
56
|
+
} from "../chunk-EP6E6CIY.js";
|
|
57
57
|
import {
|
|
58
58
|
AnchorSessionClient,
|
|
59
59
|
CONVERSATION_FAILURE_CATEGORIES,
|
|
@@ -141,7 +141,7 @@ import {
|
|
|
141
141
|
} from "../chunk-XWVM4KPK.js";
|
|
142
142
|
|
|
143
143
|
// src/lib/manager-worker.ts
|
|
144
|
-
import { createHash as
|
|
144
|
+
import { createHash as createHash13 } from "crypto";
|
|
145
145
|
import { readFileSync as readFileSync18, writeFileSync as writeFileSync9, mkdirSync as mkdirSync8, existsSync as existsSync12, rmSync as rmSync4, readdirSync as readdirSync5, statSync as statSync4, copyFileSync } from "fs";
|
|
146
146
|
import { execFileSync as syncExecFile } from "child_process";
|
|
147
147
|
import { join as join21, dirname as dirname6 } from "path";
|
|
@@ -6354,11 +6354,15 @@ function resolveSlackIngestParamsFromEnv(env, ctx) {
|
|
|
6354
6354
|
const appToken = env["SLACK_APP_TOKEN"];
|
|
6355
6355
|
const botToken = env["SLACK_BOT_TOKEN"];
|
|
6356
6356
|
if (!appToken || !botToken) return null;
|
|
6357
|
+
const ackReaction = (env["SLACK_ACK_REACTION"] ?? "").trim() || void 0;
|
|
6358
|
+
const skipReaction = (env["SLACK_SKIP_REACTION"] ?? "").trim() || void 0;
|
|
6357
6359
|
return {
|
|
6358
6360
|
appToken,
|
|
6359
6361
|
botToken,
|
|
6360
6362
|
senderPolicy: buildSlackSenderPolicyFromEnv(env, ctx),
|
|
6361
|
-
peerConfig: buildSlackPeerClassifierConfigFromEnv(env)
|
|
6363
|
+
peerConfig: buildSlackPeerClassifierConfigFromEnv(env),
|
|
6364
|
+
...ackReaction ? { ackReaction } : {},
|
|
6365
|
+
...skipReaction ? { skipReaction } : {}
|
|
6362
6366
|
};
|
|
6363
6367
|
}
|
|
6364
6368
|
var BoundedSet = class {
|
|
@@ -6396,6 +6400,12 @@ async function dispatchSlackInbound(event, ctx) {
|
|
|
6396
6400
|
ctx.log(`[slack-ingest:${ctx.codeName}] dropped ${redact(event.channel)} reason=${decision.reason}`);
|
|
6397
6401
|
return "dropped_gate";
|
|
6398
6402
|
}
|
|
6403
|
+
let ackReactionInFlight = Promise.resolve();
|
|
6404
|
+
if (ctx.ackReaction && event.channel && event.ts) {
|
|
6405
|
+
ackReactionInFlight = ctx.addReaction({ channel: event.channel, timestamp: event.ts, name: ctx.ackReaction });
|
|
6406
|
+
ackReactionInFlight.catch(() => {
|
|
6407
|
+
});
|
|
6408
|
+
}
|
|
6399
6409
|
const conversationKey = `${event.channel ?? "nochannel"}:${event.thread_ts ?? event.ts ?? ""}`;
|
|
6400
6410
|
const meta = {
|
|
6401
6411
|
source: "slack",
|
|
@@ -6423,6 +6433,23 @@ async function dispatchSlackInbound(event, ctx) {
|
|
|
6423
6433
|
});
|
|
6424
6434
|
return "admitted_replied";
|
|
6425
6435
|
}
|
|
6436
|
+
if (ctx.skipReaction && event.channel && event.ts) {
|
|
6437
|
+
const channel = event.channel;
|
|
6438
|
+
const timestamp = event.ts;
|
|
6439
|
+
const { ackReaction, skipReaction } = ctx;
|
|
6440
|
+
if (ackReaction !== skipReaction) {
|
|
6441
|
+
void (async () => {
|
|
6442
|
+
try {
|
|
6443
|
+
if (ackReaction) {
|
|
6444
|
+
await ackReactionInFlight;
|
|
6445
|
+
await ctx.removeReaction({ channel, timestamp, name: ackReaction });
|
|
6446
|
+
}
|
|
6447
|
+
await ctx.addReaction({ channel, timestamp, name: skipReaction });
|
|
6448
|
+
} catch {
|
|
6449
|
+
}
|
|
6450
|
+
})();
|
|
6451
|
+
}
|
|
6452
|
+
}
|
|
6426
6453
|
return "admitted_no_reply";
|
|
6427
6454
|
}
|
|
6428
6455
|
function redact(id) {
|
|
@@ -6448,6 +6475,46 @@ function defaultSendReply(botToken, fetchImpl, log2) {
|
|
|
6448
6475
|
}
|
|
6449
6476
|
};
|
|
6450
6477
|
}
|
|
6478
|
+
function defaultAddReaction(botToken, fetchImpl, log2) {
|
|
6479
|
+
return async ({ channel, timestamp, name }) => {
|
|
6480
|
+
try {
|
|
6481
|
+
const res = await fetchImpl(`${SLACK_API}/reactions.add`, {
|
|
6482
|
+
method: "POST",
|
|
6483
|
+
headers: {
|
|
6484
|
+
Authorization: `Bearer ${botToken}`,
|
|
6485
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
6486
|
+
},
|
|
6487
|
+
body: JSON.stringify({ channel, timestamp, name })
|
|
6488
|
+
});
|
|
6489
|
+
const data = await res.json();
|
|
6490
|
+
if (!data.ok && data.error !== "already_reacted") {
|
|
6491
|
+
log2(`[slack-ingest] reactions.add failed: ${data.error}`);
|
|
6492
|
+
}
|
|
6493
|
+
} catch (err) {
|
|
6494
|
+
log2(`[slack-ingest] reactions.add threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
6495
|
+
}
|
|
6496
|
+
};
|
|
6497
|
+
}
|
|
6498
|
+
function defaultRemoveReaction(botToken, fetchImpl, log2) {
|
|
6499
|
+
return async ({ channel, timestamp, name }) => {
|
|
6500
|
+
try {
|
|
6501
|
+
const res = await fetchImpl(`${SLACK_API}/reactions.remove`, {
|
|
6502
|
+
method: "POST",
|
|
6503
|
+
headers: {
|
|
6504
|
+
Authorization: `Bearer ${botToken}`,
|
|
6505
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
6506
|
+
},
|
|
6507
|
+
body: JSON.stringify({ channel, timestamp, name })
|
|
6508
|
+
});
|
|
6509
|
+
const data = await res.json();
|
|
6510
|
+
if (!data.ok && data.error !== "no_reaction") {
|
|
6511
|
+
log2(`[slack-ingest] reactions.remove failed: ${data.error}`);
|
|
6512
|
+
}
|
|
6513
|
+
} catch (err) {
|
|
6514
|
+
log2(`[slack-ingest] reactions.remove threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
6515
|
+
}
|
|
6516
|
+
};
|
|
6517
|
+
}
|
|
6451
6518
|
function startSlackIngest(config2) {
|
|
6452
6519
|
const fetchImpl = config2.fetchImpl ?? globalThis.fetch;
|
|
6453
6520
|
const socketFactory = config2.socketFactory ?? ((url) => {
|
|
@@ -6456,6 +6523,8 @@ function startSlackIngest(config2) {
|
|
|
6456
6523
|
return new Ctor(url);
|
|
6457
6524
|
});
|
|
6458
6525
|
const sendReply = config2.sendReply ?? defaultSendReply(config2.botToken, fetchImpl, config2.log);
|
|
6526
|
+
const addReaction = config2.addReaction ?? defaultAddReaction(config2.botToken, fetchImpl, config2.log);
|
|
6527
|
+
const removeReaction = config2.removeReaction ?? defaultRemoveReaction(config2.botToken, fetchImpl, config2.log);
|
|
6459
6528
|
const seen = new BoundedSet();
|
|
6460
6529
|
let stopped = false;
|
|
6461
6530
|
let ws = null;
|
|
@@ -6466,6 +6535,10 @@ function startSlackIngest(config2) {
|
|
|
6466
6535
|
peerConfig: config2.peerConfig,
|
|
6467
6536
|
inject: config2.inject,
|
|
6468
6537
|
sendReply,
|
|
6538
|
+
addReaction,
|
|
6539
|
+
removeReaction,
|
|
6540
|
+
...config2.ackReaction ? { ackReaction: config2.ackReaction } : {},
|
|
6541
|
+
...config2.skipReaction ? { skipReaction: config2.skipReaction } : {},
|
|
6469
6542
|
log: config2.log,
|
|
6470
6543
|
seen
|
|
6471
6544
|
};
|
|
@@ -6564,7 +6637,9 @@ function fingerprint(params) {
|
|
|
6564
6637
|
appToken: params.appToken,
|
|
6565
6638
|
botToken: params.botToken,
|
|
6566
6639
|
senderPolicy: params.senderPolicy,
|
|
6567
|
-
peerConfig: params.peerConfig
|
|
6640
|
+
peerConfig: params.peerConfig,
|
|
6641
|
+
ackReaction: params.ackReaction ?? null,
|
|
6642
|
+
skipReaction: params.skipReaction ?? null
|
|
6568
6643
|
})
|
|
6569
6644
|
).digest("hex");
|
|
6570
6645
|
}
|
|
@@ -6618,6 +6693,8 @@ async function ensureOpencodeSlackIngest(opts) {
|
|
|
6618
6693
|
peerConfig: params.peerConfig,
|
|
6619
6694
|
inject: (msg) => injectOpencodeMessage(codeName, msg, { awaitReply: true }),
|
|
6620
6695
|
log: log2,
|
|
6696
|
+
...params.ackReaction ? { ackReaction: params.ackReaction } : {},
|
|
6697
|
+
...params.skipReaction ? { skipReaction: params.skipReaction } : {},
|
|
6621
6698
|
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {},
|
|
6622
6699
|
...opts.socketFactory ? { socketFactory: opts.socketFactory } : {}
|
|
6623
6700
|
});
|
|
@@ -6635,6 +6712,98 @@ function stopAllOpencodeSlackIngests(log2) {
|
|
|
6635
6712
|
for (const codeName of [...ingests.keys()]) stopOpencodeSlackIngest(codeName, log2);
|
|
6636
6713
|
}
|
|
6637
6714
|
|
|
6715
|
+
// src/lib/manager/opencode-scheduler.ts
|
|
6716
|
+
import { createHash as createHash12 } from "crypto";
|
|
6717
|
+
var MAX_OPENCODE_SCHED_CONCURRENCY = 2;
|
|
6718
|
+
var opencodeSchedulerStates = /* @__PURE__ */ new Map();
|
|
6719
|
+
var inFlightOpencodeTasks = /* @__PURE__ */ new Set();
|
|
6720
|
+
var opencodeTaskConcurrency = /* @__PURE__ */ new Map();
|
|
6721
|
+
var knownOpencodeTaskHashes = /* @__PURE__ */ new Map();
|
|
6722
|
+
function shouldDeliver(task, reply) {
|
|
6723
|
+
if ((task.deliveryPolicy ?? "always") === "never") return false;
|
|
6724
|
+
if (task.deliveryMode === "none") return false;
|
|
6725
|
+
if (task.deliveryTo == null) return false;
|
|
6726
|
+
return typeof reply === "string" && reply.trim().length > 0;
|
|
6727
|
+
}
|
|
6728
|
+
async function syncAndCheckOpencodeScheduler(agent, tasks, refreshData) {
|
|
6729
|
+
const codeName = agent.code_name;
|
|
6730
|
+
const tasksHash = createHash12("sha256").update(JSON.stringify(tasks)).digest("hex").slice(0, 16);
|
|
6731
|
+
if (knownOpencodeTaskHashes.get(agent.agent_id) !== tasksHash) {
|
|
6732
|
+
const taskInputs = tasks.map((t) => buildSchedulerTaskInput(t));
|
|
6733
|
+
const state8 = syncTasksToScheduler(codeName, agent.agent_id, taskInputs);
|
|
6734
|
+
opencodeSchedulerStates.set(codeName, state8);
|
|
6735
|
+
knownOpencodeTaskHashes.set(agent.agent_id, tasksHash);
|
|
6736
|
+
log(`[opencode-scheduler] tasks synced for '${codeName}' (${taskInputs.length} task(s))`);
|
|
6737
|
+
}
|
|
6738
|
+
if (!opencodeSchedulerStates.has(codeName)) {
|
|
6739
|
+
opencodeSchedulerStates.set(codeName, loadSchedulerState(codeName));
|
|
6740
|
+
}
|
|
6741
|
+
const state7 = opencodeSchedulerStates.get(codeName);
|
|
6742
|
+
const ready = getReadyTasks(state7, inFlightOpencodeTasks);
|
|
6743
|
+
if (ready.length === 0) return;
|
|
6744
|
+
if (!isOpencodeSessionHealthy(codeName)) {
|
|
6745
|
+
log(`[opencode-scheduler] '${codeName}' has ${ready.length} due task(s) but no healthy serve yet - deferring`);
|
|
6746
|
+
return;
|
|
6747
|
+
}
|
|
6748
|
+
for (const task of ready) {
|
|
6749
|
+
if ((opencodeTaskConcurrency.get(codeName) ?? 0) >= MAX_OPENCODE_SCHED_CONCURRENCY) break;
|
|
6750
|
+
if (KANBAN_WORK_TEMPLATES.has(task.templateId)) {
|
|
6751
|
+
opencodeSchedulerStates.set(codeName, markTaskFired(codeName, task.taskId, "ok"));
|
|
6752
|
+
continue;
|
|
6753
|
+
}
|
|
6754
|
+
inFlightOpencodeTasks.add(task.taskId);
|
|
6755
|
+
opencodeTaskConcurrency.set(codeName, (opencodeTaskConcurrency.get(codeName) ?? 0) + 1);
|
|
6756
|
+
void fireOpencodeScheduledTask(agent, task).finally(() => {
|
|
6757
|
+
inFlightOpencodeTasks.delete(task.taskId);
|
|
6758
|
+
opencodeTaskConcurrency.set(
|
|
6759
|
+
codeName,
|
|
6760
|
+
Math.max(0, (opencodeTaskConcurrency.get(codeName) ?? 1) - 1)
|
|
6761
|
+
);
|
|
6762
|
+
});
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
async function fireOpencodeScheduledTask(agent, task) {
|
|
6766
|
+
const codeName = agent.code_name;
|
|
6767
|
+
const conversationKey = `scheduled:${task.taskId}:${Date.now()}`;
|
|
6768
|
+
try {
|
|
6769
|
+
const result = await injectOpencodeMessage(
|
|
6770
|
+
codeName,
|
|
6771
|
+
{
|
|
6772
|
+
channelId: "scheduler",
|
|
6773
|
+
conversationKey,
|
|
6774
|
+
senderId: "scheduler",
|
|
6775
|
+
text: task.prompt,
|
|
6776
|
+
meta: {
|
|
6777
|
+
source: "scheduled_task",
|
|
6778
|
+
task: task.name,
|
|
6779
|
+
// The manager delivers the output; the agent isn't replying into a
|
|
6780
|
+
// channel thread itself, so it must not try to.
|
|
6781
|
+
requires_reply: "false"
|
|
6782
|
+
}
|
|
6783
|
+
},
|
|
6784
|
+
{ awaitReply: true }
|
|
6785
|
+
);
|
|
6786
|
+
if (result.status === "declined") {
|
|
6787
|
+
log(`[opencode-scheduler] task '${task.name}' for '${codeName}' declined: ${result.reason}`);
|
|
6788
|
+
return;
|
|
6789
|
+
}
|
|
6790
|
+
const reply = result.status === "replied" ? result.reply : null;
|
|
6791
|
+
if (shouldDeliver(task, reply)) {
|
|
6792
|
+
try {
|
|
6793
|
+
await deliverScheduledTaskOutput(codeName, agent.agent_id, task.deliveryTo, reply, task.taskId);
|
|
6794
|
+
} catch (err) {
|
|
6795
|
+
log(`[opencode-scheduler] delivery failed for task '${task.name}' on '${codeName}': ${err.message}`);
|
|
6796
|
+
}
|
|
6797
|
+
} else if ((task.deliveryPolicy ?? "always") !== "never" && task.deliveryTo != null) {
|
|
6798
|
+
log(`[opencode-scheduler] task '${task.name}' for '${codeName}' produced no output to deliver`);
|
|
6799
|
+
}
|
|
6800
|
+
opencodeSchedulerStates.set(codeName, markTaskFired(codeName, task.taskId, "ok"));
|
|
6801
|
+
} catch (err) {
|
|
6802
|
+
log(`[opencode-scheduler] task '${task.name}' for '${codeName}' failed: ${err.message}`);
|
|
6803
|
+
opencodeSchedulerStates.set(codeName, markTaskFired(codeName, task.taskId, "error"));
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
|
|
6638
6807
|
// src/lib/wedge-detection.ts
|
|
6639
6808
|
var DEFAULTS = {
|
|
6640
6809
|
inboundWaitSeconds: 120,
|
|
@@ -8045,7 +8214,7 @@ var sessionLaunchManagedStructure = /* @__PURE__ */ new Map();
|
|
|
8045
8214
|
function projectMcpHash(_codeName, projectDir) {
|
|
8046
8215
|
try {
|
|
8047
8216
|
const raw = readFileSync18(join21(projectDir, ".mcp.json"), "utf-8");
|
|
8048
|
-
return
|
|
8217
|
+
return createHash13("sha256").update(canonicalJson(JSON.parse(raw))).digest("hex");
|
|
8049
8218
|
} catch {
|
|
8050
8219
|
return null;
|
|
8051
8220
|
}
|
|
@@ -8391,7 +8560,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
|
|
|
8391
8560
|
var lastVersionCheckAt = 0;
|
|
8392
8561
|
var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
8393
8562
|
var lastResponsivenessProbeAt = 0;
|
|
8394
|
-
var agtCliVersion = true ? "0.28.
|
|
8563
|
+
var agtCliVersion = true ? "0.28.343" : "dev";
|
|
8395
8564
|
function resolveBrewPath(execFileSync2) {
|
|
8396
8565
|
try {
|
|
8397
8566
|
const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
@@ -9485,7 +9654,7 @@ async function pollCycle() {
|
|
|
9485
9654
|
claudeAuth = await detectClaudeAuth();
|
|
9486
9655
|
} catch (err) {
|
|
9487
9656
|
const errText = err instanceof Error ? err.message : String(err);
|
|
9488
|
-
const errId =
|
|
9657
|
+
const errId = createHash13("sha256").update(errText).digest("hex").slice(0, 12);
|
|
9489
9658
|
log(`Claude auth detection failed (error_id=${errId})`);
|
|
9490
9659
|
}
|
|
9491
9660
|
const hostHasClaudeCode = state6.agents.some(
|
|
@@ -9643,7 +9812,7 @@ async function pollCycle() {
|
|
|
9643
9812
|
livePendingInboundOldestAgeSeconds,
|
|
9644
9813
|
parkPendingInbound
|
|
9645
9814
|
} = await import("../responsiveness-probe-XLS2FXIX.js");
|
|
9646
|
-
const { getProjectDir: wedgeProjectDir } = await import("../
|
|
9815
|
+
const { getProjectDir: wedgeProjectDir } = await import("../scheduler-engine-HYOOQV4A.js");
|
|
9647
9816
|
const wedgeNow = /* @__PURE__ */ new Date();
|
|
9648
9817
|
const liveAgents = agentState.persistentSessionAgents;
|
|
9649
9818
|
for (const tracked of consecutiveWedgeCycles.keys()) {
|
|
@@ -10094,6 +10263,15 @@ async function pollCycle() {
|
|
|
10094
10263
|
pollDirectChatMessages(agentStates).catch((err) => {
|
|
10095
10264
|
log(`Direct chat poll error: ${err.message}`);
|
|
10096
10265
|
});
|
|
10266
|
+
} else {
|
|
10267
|
+
const opencodeAgents = agentStates.filter(
|
|
10268
|
+
(a) => (agentFrameworkCache.get(a.codeName) ?? DEFAULT_FRAMEWORK) === "opencode"
|
|
10269
|
+
);
|
|
10270
|
+
if (opencodeAgents.length > 0) {
|
|
10271
|
+
pollDirectChatMessages(opencodeAgents).catch((err) => {
|
|
10272
|
+
log(`Direct chat poll error (opencode): ${err.message}`);
|
|
10273
|
+
});
|
|
10274
|
+
}
|
|
10097
10275
|
}
|
|
10098
10276
|
ensureRealtimeStarted(agentStates);
|
|
10099
10277
|
ensureRealtimeDriftStarted(agentStates);
|
|
@@ -10896,7 +11074,7 @@ async function processAgent(agent, agentStates) {
|
|
|
10896
11074
|
const behaviourSubset = extractMsTeamsBehaviourSubset(
|
|
10897
11075
|
msteamsEntry?.config
|
|
10898
11076
|
);
|
|
10899
|
-
const behaviourHash =
|
|
11077
|
+
const behaviourHash = createHash13("sha256").update(canonicalJson(behaviourSubset)).digest("hex");
|
|
10900
11078
|
const prevBehaviourHash = agentState.knownMsTeamsBehaviourHashes.get(agent.agent_id);
|
|
10901
11079
|
const msteamsBehaviourRestrictive = isMsTeamsBehaviourRestrictive(behaviourSubset);
|
|
10902
11080
|
const behaviourDecision = decideSenderPolicyRestart({
|
|
@@ -10952,7 +11130,7 @@ async function processAgent(agent, agentStates) {
|
|
|
10952
11130
|
const slackBehaviourSubset = extractSlackBehaviourSubset(
|
|
10953
11131
|
slackEntry?.config
|
|
10954
11132
|
);
|
|
10955
|
-
const slackBehaviourHash =
|
|
11133
|
+
const slackBehaviourHash = createHash13("sha256").update(canonicalJson(slackBehaviourSubset)).digest("hex");
|
|
10956
11134
|
const prevSlackBehaviourHash = agentState.knownSlackBehaviourHashes.get(agent.agent_id);
|
|
10957
11135
|
const slackBehaviourRestrictive = isSlackBehaviourRestrictive(slackBehaviourSubset);
|
|
10958
11136
|
const slackBehaviourDecision = decideSenderPolicyRestart({
|
|
@@ -11302,10 +11480,10 @@ async function processAgent(agent, agentStates) {
|
|
|
11302
11480
|
desiredEntries.push({ serverId, url, headers: mcpHeaders, name: tk.toolkit_name });
|
|
11303
11481
|
}
|
|
11304
11482
|
const hashBasis = desiredEntries.slice().sort((a, b) => a.serverId.localeCompare(b.serverId)).map((e) => {
|
|
11305
|
-
const headersHash =
|
|
11483
|
+
const headersHash = createHash13("sha256").update(canonicalJson(e.headers ?? {})).digest("hex").slice(0, 16);
|
|
11306
11484
|
return `${e.serverId}|${e.url}|${headersHash}`;
|
|
11307
11485
|
}).join("\n");
|
|
11308
|
-
const mcpHash =
|
|
11486
|
+
const mcpHash = createHash13("sha256").update(hashBasis).digest("hex").slice(0, 16);
|
|
11309
11487
|
const prevMcpHash = agentState.knownManagedMcpHashes.get(agent.agent_id);
|
|
11310
11488
|
const structureHash = managedMcpStructureHash(desiredEntries);
|
|
11311
11489
|
const prevStructureHash = agentState.knownManagedMcpStructure.get(agent.agent_id);
|
|
@@ -11320,7 +11498,7 @@ async function processAgent(agent, agentStates) {
|
|
|
11320
11498
|
if (mcpHash !== prevMcpHash) {
|
|
11321
11499
|
for (const e of desiredEntries) {
|
|
11322
11500
|
frameworkAdapter.writeMcpServer(agent.code_name, e.serverId, { url: e.url, headers: e.headers });
|
|
11323
|
-
const urlHash =
|
|
11501
|
+
const urlHash = createHash13("sha256").update(e.url).digest("hex").slice(0, 12);
|
|
11324
11502
|
log(`[managed-toolkit] ${agent.code_name}: wrote '${e.name}' (serverId=${e.serverId}, url_hash=${urlHash})`);
|
|
11325
11503
|
}
|
|
11326
11504
|
if (frameworkAdapter.removeMcpServer && frameworkAdapter.getMcpPath) {
|
|
@@ -11429,7 +11607,7 @@ async function processAgent(agent, agentStates) {
|
|
|
11429
11607
|
if (frameworkAdapter.installSkillFiles) {
|
|
11430
11608
|
const currentIntegrationSkillIds = /* @__PURE__ */ new Set();
|
|
11431
11609
|
const installedIntegrationSkills = [];
|
|
11432
|
-
const { createHash:
|
|
11610
|
+
const { createHash: createHash14 } = await import("crypto");
|
|
11433
11611
|
const refreshAny = refreshData;
|
|
11434
11612
|
const contexts = refreshAny.integration_contexts ?? refreshAny.plugin_contexts ?? [];
|
|
11435
11613
|
const contextBySlug = /* @__PURE__ */ new Map();
|
|
@@ -11458,7 +11636,7 @@ async function processAgent(agent, agentStates) {
|
|
|
11458
11636
|
)
|
|
11459
11637
|
}));
|
|
11460
11638
|
const bundle = buildIntegrationBundle(renderedScopes);
|
|
11461
|
-
const contentHash =
|
|
11639
|
+
const contentHash = createHash14("sha256").update(bundleFingerprint(bundle.files)).digest("hex").slice(0, 12);
|
|
11462
11640
|
const hashKey = `plugin-skill:${agent.agent_id}:${integrationSkillId}`;
|
|
11463
11641
|
if (agentState.knownSkillHashes.get(hashKey) === contentHash) continue;
|
|
11464
11642
|
frameworkAdapter.installSkillFiles(agent.code_name, integrationSkillId, bundle.files);
|
|
@@ -11525,7 +11703,7 @@ async function processAgent(agent, agentStates) {
|
|
|
11525
11703
|
const plan = planGlobalSkillSync(
|
|
11526
11704
|
[...globalSkillsPayload ?? [], ...sharedSkillsPayload ?? []],
|
|
11527
11705
|
prevIds,
|
|
11528
|
-
(content) =>
|
|
11706
|
+
(content) => createHash14("sha256").update(content).digest("hex").slice(0, 12),
|
|
11529
11707
|
(skillId) => agentState.knownSkillHashes.get(`global-skill:${agent.agent_id}:${skillId}`),
|
|
11530
11708
|
{ desiredResolved }
|
|
11531
11709
|
);
|
|
@@ -11576,7 +11754,7 @@ async function processAgent(agent, agentStates) {
|
|
|
11576
11754
|
const slug = hook.integration_slug ?? hook.plugin_slug;
|
|
11577
11755
|
if (!slug) continue;
|
|
11578
11756
|
try {
|
|
11579
|
-
const scriptHash =
|
|
11757
|
+
const scriptHash = createHash14("sha256").update(hook.script).digest("hex").slice(0, 12);
|
|
11580
11758
|
const hookKey = `${agent.agent_id}:${frameworkAdapter.id}:plugin-hook:${slug}:on_install`;
|
|
11581
11759
|
if (agentState.knownSkillHashes.get(hookKey) === scriptHash) continue;
|
|
11582
11760
|
const result = await frameworkAdapter.executePluginHook({
|
|
@@ -11591,9 +11769,9 @@ async function processAgent(agent, agentStates) {
|
|
|
11591
11769
|
} else if (result.timedOut) {
|
|
11592
11770
|
log(`Integration hook on_install '${slug}' TIMED OUT for '${agent.code_name}' after ${result.durationMs}ms`);
|
|
11593
11771
|
} else {
|
|
11594
|
-
const stderrHash =
|
|
11772
|
+
const stderrHash = createHash14("sha256").update(result.stderr).digest("hex").slice(0, 12);
|
|
11595
11773
|
const missingCmd = result.exitCode === 127 ? extractCommandNotFound(result.stderr) : null;
|
|
11596
|
-
const missingCmdHash = missingCmd ?
|
|
11774
|
+
const missingCmdHash = missingCmd ? createHash14("sha256").update(missingCmd).digest("hex").slice(0, 8) : null;
|
|
11597
11775
|
log(
|
|
11598
11776
|
`Integration hook on_install '${slug}' exited ${result.exitCode} for '${agent.code_name}' ` + (missingCmdHash ? `[missing_command_hash=${missingCmdHash}] ` : "") + `[stderr_hash=${stderrHash} stderr_len=${result.stderr.length}]`
|
|
11599
11777
|
);
|
|
@@ -11701,7 +11879,7 @@ async function processAgent(agent, agentStates) {
|
|
|
11701
11879
|
}
|
|
11702
11880
|
}
|
|
11703
11881
|
}
|
|
11704
|
-
if (agentFw === "claude-code" && sessionMode === "persistent") {
|
|
11882
|
+
if (agentFw === "claude-code" && sessionMode === "persistent" || agentFw === "opencode") {
|
|
11705
11883
|
const wasSessionHealthy = isSessionHealthy(agent.code_name);
|
|
11706
11884
|
let psResult;
|
|
11707
11885
|
try {
|
|
@@ -11733,167 +11911,172 @@ async function processAgent(agent, agentStates) {
|
|
|
11733
11911
|
} else if (stuck.recovered) {
|
|
11734
11912
|
log(`[persistent-session-stuck] agent=${agent.code_name} recovered from stuck state`);
|
|
11735
11913
|
}
|
|
11736
|
-
|
|
11737
|
-
checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
|
|
11738
|
-
} catch (err) {
|
|
11739
|
-
log(`[hot-reload] .mcp.json drift check failed for '${agent.code_name}': ${err.message}`);
|
|
11740
|
-
}
|
|
11741
|
-
if (channelConfigConverged) {
|
|
11914
|
+
if (agentFw === "claude-code") {
|
|
11742
11915
|
try {
|
|
11743
|
-
|
|
11916
|
+
checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
|
|
11744
11917
|
} catch (err) {
|
|
11745
|
-
log(`[hot-reload]
|
|
11918
|
+
log(`[hot-reload] .mcp.json drift check failed for '${agent.code_name}': ${err.message}`);
|
|
11746
11919
|
}
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
mcpJsonParsed = JSON.parse(readFileSync18(mcpPath, "utf-8"));
|
|
11754
|
-
} catch {
|
|
11920
|
+
if (channelConfigConverged) {
|
|
11921
|
+
try {
|
|
11922
|
+
checkChannelSecretDriftAndScheduleRestart(agent.code_name, getProjectDir(agent.code_name));
|
|
11923
|
+
} catch (err) {
|
|
11924
|
+
log(`[hot-reload] channel-secret drift check failed for '${agent.code_name}': ${err.message}`);
|
|
11925
|
+
}
|
|
11755
11926
|
}
|
|
11756
|
-
|
|
11757
|
-
|
|
11758
|
-
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
|
|
11794
|
-
|
|
11795
|
-
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
// toolkits poll drops it from the response → next writeMcpServer
|
|
11799
|
-
// omits it → broken MCP is cleanly gone instead of declared-but-
|
|
11800
|
-
// dead.
|
|
11801
|
-
//
|
|
11802
|
-
// Async fire-and-forget — the reaper's correctness must not
|
|
11803
|
-
// depend on the API call succeeding. Errors are caught inside
|
|
11804
|
-
// the .then/.catch chain and logged.
|
|
11805
|
-
onGiveUp: (codeName, serverKey) => {
|
|
11806
|
-
const mapping = managedToolkitIdByAgentAndServerId.get(
|
|
11807
|
-
managedToolkitMapKey(agent.agent_id, serverKey)
|
|
11808
|
-
);
|
|
11809
|
-
if (!mapping) {
|
|
11810
|
-
log(
|
|
11811
|
-
`[mcp-presence-reaper] give-up on '${codeName}:${serverKey}' \u2014 not a managed toolkit (no toolkit_id mapping), skipping integration-health API call`
|
|
11812
|
-
);
|
|
11813
|
-
return;
|
|
11814
|
-
}
|
|
11815
|
-
const { toolkitId, connectionKey } = mapping;
|
|
11816
|
-
const agentId = agent.agent_id;
|
|
11817
|
-
api.post("/host/integration-health", {
|
|
11818
|
-
agent_id: agentId,
|
|
11819
|
-
toolkit_id: toolkitId,
|
|
11820
|
-
// ENG-7543: scope the mark-unhealthy to the specific connection whose
|
|
11821
|
-
// MCP server gave up, so a dead 2nd mailbox doesn't disable the first.
|
|
11822
|
-
connection_key: connectionKey,
|
|
11823
|
-
action: "mark_unhealthy",
|
|
11824
|
-
reason: `mcp-presence-reaper cap (${3} consecutive failed restart cycles) tripped on '${serverKey}'`
|
|
11825
|
-
}).then((res) => {
|
|
11826
|
-
log(
|
|
11827
|
-
`[mcp-presence-reaper] marked integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${JSON.stringify(res).slice(0, 200)}`
|
|
11828
|
-
);
|
|
11829
|
-
}).catch((err) => {
|
|
11830
|
-
log(
|
|
11831
|
-
`[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`
|
|
11832
|
-
);
|
|
11833
|
-
});
|
|
11834
|
-
},
|
|
11835
|
-
// ENG-5286: record the restart event with the API so the
|
|
11836
|
-
// agent-heartbeat-monitor cron can aggregate per-agent counts
|
|
11837
|
-
// into the McpReaperRestartsHourly CloudWatch metric. Fire-and-
|
|
11838
|
-
// forget — the reaper's correctness must not depend on this.
|
|
11839
|
-
onRestart: (codeName, { activeKeys, givenUpKeys }) => {
|
|
11840
|
-
api.post("/host/mcp-reaper-restart", {
|
|
11841
|
-
agent_id: agent.agent_id,
|
|
11842
|
-
code_name: codeName,
|
|
11843
|
-
server_keys: activeKeys,
|
|
11844
|
-
given_up_keys: givenUpKeys
|
|
11845
|
-
}).catch((err) => {
|
|
11846
|
-
log(
|
|
11847
|
-
`[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`
|
|
11848
|
-
);
|
|
11849
|
-
});
|
|
11850
|
-
void reportReaperEvent(
|
|
11851
|
-
agent.agent_id,
|
|
11852
|
-
"mcp-presence-reaper",
|
|
11853
|
-
"restart",
|
|
11854
|
-
givenUpKeys.length > 0 ? `restarted session; MCP server(s) still down after restart budget: ${givenUpKeys.join(", ")}` : `restarted session to recover MCP server(s): ${activeKeys.join(", ")}`
|
|
11855
|
-
);
|
|
11856
|
-
},
|
|
11857
|
-
// ENG-5932: classify each declared key for the quarantine path. Reads a
|
|
11858
|
-
// server-declared criticality field off the .mcp.json entry when
|
|
11859
|
-
// present (Slice 2 will stamp it); otherwise fails closed to ESSENTIAL
|
|
11860
|
-
// for everything except the known optional channel set. Tool servers
|
|
11861
|
-
// (composio_*/xero) classify ESSENTIAL here and stay on the existing
|
|
11862
|
-
// managed-toolkit onGiveUp path above.
|
|
11863
|
-
classifyKey: (serverKey) => classifyChannelCriticality(
|
|
11864
|
-
serverKey,
|
|
11865
|
-
mcpJsonParsed?.mcpServers?.[serverKey]
|
|
11866
|
-
),
|
|
11867
|
-
quarantineDwellMs: channelQuarantineDwellMs(),
|
|
11868
|
-
quarantineMode: channelQuarantineMode(),
|
|
11869
|
-
// ENG-5932: enforce-mode quarantine. Persist the local marker; the next
|
|
11870
|
-
// provisioning poll then omits the channel from .mcp.json + the launch
|
|
11871
|
-
// flags, and the channel-set-change / .mcp.json-drift restart paths are
|
|
11872
|
-
// taught to NOT bounce the session for a quarantine-driven removal.
|
|
11873
|
-
// Local-authoritative: no API call — with the control plane down, the
|
|
11874
|
-
// flap still stops.
|
|
11875
|
-
onQuarantine: (codeName, serverKey) => {
|
|
11876
|
-
const newly = channelQuarantineStore().quarantine(
|
|
11927
|
+
try {
|
|
11928
|
+
const sess = getSessionState(agent.code_name);
|
|
11929
|
+
let mcpJsonParsed = null;
|
|
11930
|
+
try {
|
|
11931
|
+
const mcpPath = join21(getProjectDir(agent.code_name), ".mcp.json");
|
|
11932
|
+
mcpJsonParsed = JSON.parse(readFileSync18(mcpPath, "utf-8"));
|
|
11933
|
+
} catch {
|
|
11934
|
+
}
|
|
11935
|
+
reapMissingMcpSessions({
|
|
11936
|
+
log,
|
|
11937
|
+
codeName: agent.code_name,
|
|
11938
|
+
mcpJson: mcpJsonParsed,
|
|
11939
|
+
sessionStartedAt: sess?.startedAt ?? null,
|
|
11940
|
+
// ENG-6660: under Docker the MCP children live in the agent's container
|
|
11941
|
+
// PID namespace - probe inside it (docker exec ps) instead of the host,
|
|
11942
|
+
// or every declared MCP looks "missing" -> restart loop -> flapping.
|
|
11943
|
+
isolated: isolationMode(agent.code_name) === "docker",
|
|
11944
|
+
// ENG-6486: a restart is already scheduled/deferred for this agent (e.g.
|
|
11945
|
+
// a freshly-added channel awaiting its convergence restart, deferred by
|
|
11946
|
+
// the maintenance window). The pending restart will spawn the declared-
|
|
11947
|
+
// but-not-yet-live MCP, so don't let the reaper re-fire + inflate the
|
|
11948
|
+
// McpReaperRestartsHourly metric into a false flapping alarm.
|
|
11949
|
+
restartPending: pendingSessionRestarts.has(agent.code_name),
|
|
11950
|
+
// ENG-5441 / ENG-5547: a mcp-presence-reaper restart counts against
|
|
11951
|
+
// the agent-wide breaker ONLY when several distinct MCP servers are
|
|
11952
|
+
// missing at once — a thrashing collection that rotates restarts of
|
|
11953
|
+
// the whole session, which the per-MCP cap can't see. A restart
|
|
11954
|
+
// driven by a SINGLE persistently-failing server is left to that
|
|
11955
|
+
// server's own ENG-5279 give-up cap (→ marked unhealthy via onGiveUp
|
|
11956
|
+
// → dropped from the next .mcp.json), so one bad integration is
|
|
11957
|
+
// isolated and the agent keeps running degraded instead of being
|
|
11958
|
+
// paused wholesale. reaperRestartBreakerReason() encodes that
|
|
11959
|
+
// single-vs-multi decision; undefined means "restart, don't count".
|
|
11960
|
+
//
|
|
11961
|
+
// ENG-6264: the breaker-count reason (above, undefined for a single dead
|
|
11962
|
+
// MCP) is decoupled from the GATE reason. Pre-6264 an undefined breaker
|
|
11963
|
+
// reason also made the restart non-gateable → 'bypass' → the session was
|
|
11964
|
+
// torn down mid-turn (the common single-MCP case interrupted busy
|
|
11965
|
+
// agents). Always pass 'mcp-presence-reaper' as the gate reason so the
|
|
11966
|
+
// restart defers-until-idle, while breakerReason still governs whether it
|
|
11967
|
+
// counts against the breaker.
|
|
11968
|
+
stopSession: (codeName, ctx) => stopPersistentSessionAndForgetMcpBaseline(
|
|
11877
11969
|
codeName,
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11970
|
+
reaperRestartBreakerReason(ctx.activeKeys),
|
|
11971
|
+
"mcp-presence-reaper"
|
|
11972
|
+
),
|
|
11973
|
+
// ENG-5292: when the reaper gives up on a managed MCP (cap from
|
|
11974
|
+
// ENG-5279 + state-preservation from ENG-5285 both said "this
|
|
11975
|
+
// MCP keeps failing after 3 restart cycles"), mark the matching
|
|
11976
|
+
// integration unhealthy server-side. The next /host/managed-
|
|
11977
|
+
// toolkits poll drops it from the response → next writeMcpServer
|
|
11978
|
+
// omits it → broken MCP is cleanly gone instead of declared-but-
|
|
11979
|
+
// dead.
|
|
11980
|
+
//
|
|
11981
|
+
// Async fire-and-forget — the reaper's correctness must not
|
|
11982
|
+
// depend on the API call succeeding. Errors are caught inside
|
|
11983
|
+
// the .then/.catch chain and logged.
|
|
11984
|
+
onGiveUp: (codeName, serverKey) => {
|
|
11985
|
+
const mapping = managedToolkitIdByAgentAndServerId.get(
|
|
11986
|
+
managedToolkitMapKey(agent.agent_id, serverKey)
|
|
11885
11987
|
);
|
|
11988
|
+
if (!mapping) {
|
|
11989
|
+
log(
|
|
11990
|
+
`[mcp-presence-reaper] give-up on '${codeName}:${serverKey}' \u2014 not a managed toolkit (no toolkit_id mapping), skipping integration-health API call`
|
|
11991
|
+
);
|
|
11992
|
+
return;
|
|
11993
|
+
}
|
|
11994
|
+
const { toolkitId, connectionKey } = mapping;
|
|
11995
|
+
const agentId = agent.agent_id;
|
|
11996
|
+
api.post("/host/integration-health", {
|
|
11997
|
+
agent_id: agentId,
|
|
11998
|
+
toolkit_id: toolkitId,
|
|
11999
|
+
// ENG-7543: scope the mark-unhealthy to the specific connection whose
|
|
12000
|
+
// MCP server gave up, so a dead 2nd mailbox doesn't disable the first.
|
|
12001
|
+
connection_key: connectionKey,
|
|
12002
|
+
action: "mark_unhealthy",
|
|
12003
|
+
reason: `mcp-presence-reaper cap (${3} consecutive failed restart cycles) tripped on '${serverKey}'`
|
|
12004
|
+
}).then((res) => {
|
|
12005
|
+
log(
|
|
12006
|
+
`[mcp-presence-reaper] marked integration unhealthy for '${codeName}' toolkit '${toolkitId}' (ENG-5292): ${JSON.stringify(res).slice(0, 200)}`
|
|
12007
|
+
);
|
|
12008
|
+
}).catch((err) => {
|
|
12009
|
+
log(
|
|
12010
|
+
`[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`
|
|
12011
|
+
);
|
|
12012
|
+
});
|
|
12013
|
+
},
|
|
12014
|
+
// ENG-5286: record the restart event with the API so the
|
|
12015
|
+
// agent-heartbeat-monitor cron can aggregate per-agent counts
|
|
12016
|
+
// into the McpReaperRestartsHourly CloudWatch metric. Fire-and-
|
|
12017
|
+
// forget — the reaper's correctness must not depend on this.
|
|
12018
|
+
onRestart: (codeName, { activeKeys, givenUpKeys }) => {
|
|
12019
|
+
api.post("/host/mcp-reaper-restart", {
|
|
12020
|
+
agent_id: agent.agent_id,
|
|
12021
|
+
code_name: codeName,
|
|
12022
|
+
server_keys: activeKeys,
|
|
12023
|
+
given_up_keys: givenUpKeys
|
|
12024
|
+
}).catch((err) => {
|
|
12025
|
+
log(
|
|
12026
|
+
`[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`
|
|
12027
|
+
);
|
|
12028
|
+
});
|
|
11886
12029
|
void reportReaperEvent(
|
|
11887
12030
|
agent.agent_id,
|
|
11888
12031
|
"mcp-presence-reaper",
|
|
11889
|
-
"
|
|
11890
|
-
`
|
|
12032
|
+
"restart",
|
|
12033
|
+
givenUpKeys.length > 0 ? `restarted session; MCP server(s) still down after restart budget: ${givenUpKeys.join(", ")}` : `restarted session to recover MCP server(s): ${activeKeys.join(", ")}`
|
|
12034
|
+
);
|
|
12035
|
+
},
|
|
12036
|
+
// ENG-5932: classify each declared key for the quarantine path. Reads a
|
|
12037
|
+
// server-declared criticality field off the .mcp.json entry when
|
|
12038
|
+
// present (Slice 2 will stamp it); otherwise fails closed to ESSENTIAL
|
|
12039
|
+
// for everything except the known optional channel set. Tool servers
|
|
12040
|
+
// (composio_*/xero) classify ESSENTIAL here and stay on the existing
|
|
12041
|
+
// managed-toolkit onGiveUp path above.
|
|
12042
|
+
classifyKey: (serverKey) => classifyChannelCriticality(
|
|
12043
|
+
serverKey,
|
|
12044
|
+
mcpJsonParsed?.mcpServers?.[serverKey]
|
|
12045
|
+
),
|
|
12046
|
+
quarantineDwellMs: channelQuarantineDwellMs(),
|
|
12047
|
+
quarantineMode: channelQuarantineMode(),
|
|
12048
|
+
// ENG-5932: enforce-mode quarantine. Persist the local marker; the next
|
|
12049
|
+
// provisioning poll then omits the channel from .mcp.json + the launch
|
|
12050
|
+
// flags, and the channel-set-change / .mcp.json-drift restart paths are
|
|
12051
|
+
// taught to NOT bounce the session for a quarantine-driven removal.
|
|
12052
|
+
// Local-authoritative: no API call — with the control plane down, the
|
|
12053
|
+
// flap still stops.
|
|
12054
|
+
onQuarantine: (codeName, serverKey) => {
|
|
12055
|
+
const newly = channelQuarantineStore().quarantine(
|
|
12056
|
+
codeName,
|
|
12057
|
+
serverKey,
|
|
12058
|
+
`mcp-presence-reaper: optional channel dead past restart budget + dwell`,
|
|
12059
|
+
Date.now()
|
|
11891
12060
|
);
|
|
12061
|
+
if (newly) {
|
|
12062
|
+
log(
|
|
12063
|
+
`[channel-quarantine] persisted quarantine marker for '${codeName}:${serverKey}' \u2014 will be dropped from .mcp.json on next provisioning poll (ENG-5932)`
|
|
12064
|
+
);
|
|
12065
|
+
void reportReaperEvent(
|
|
12066
|
+
agent.agent_id,
|
|
12067
|
+
"mcp-presence-reaper",
|
|
12068
|
+
"quarantine",
|
|
12069
|
+
`optional channel '${serverKey}' dead past restart budget + dwell \u2014 quarantined`
|
|
12070
|
+
);
|
|
12071
|
+
}
|
|
11892
12072
|
}
|
|
11893
|
-
}
|
|
11894
|
-
})
|
|
11895
|
-
|
|
11896
|
-
|
|
12073
|
+
});
|
|
12074
|
+
} catch (err) {
|
|
12075
|
+
log(`[mcp-presence-reaper] presence check failed for '${agent.code_name}': ${err.message}`);
|
|
12076
|
+
}
|
|
12077
|
+
}
|
|
12078
|
+
if (agentFw === "opencode" && tasks.length > 0) {
|
|
12079
|
+
await syncAndCheckOpencodeScheduler(agent, tasks, refreshData);
|
|
11897
12080
|
}
|
|
11898
12081
|
} else if (agentFw === "claude-code" && tasks.length > 0) {
|
|
11899
12082
|
await syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData);
|
|
@@ -12161,7 +12344,7 @@ async function ensureOpencodeRuntime(agent, refreshData, agentTimezone) {
|
|
|
12161
12344
|
return { decision: "healthy", spawnAttempted: false, sessionHealthyAfter: true };
|
|
12162
12345
|
}
|
|
12163
12346
|
stopOpencodeSlackIngest(codeName, log);
|
|
12164
|
-
const opencodeProjectDir = join21(getFramework("opencode").getAgentDir(codeName), "
|
|
12347
|
+
const opencodeProjectDir = join21(getFramework("opencode").getAgentDir(codeName), "provision");
|
|
12165
12348
|
const openRouterRaw = refreshData.openrouter ?? null;
|
|
12166
12349
|
const serveEnv = {
|
|
12167
12350
|
AGT_HOST: requireHost(),
|
|
@@ -12360,7 +12543,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
|
|
|
12360
12543
|
const ctx = getLastFailureContext(codeName);
|
|
12361
12544
|
const recovery = prepareForRespawn(codeName);
|
|
12362
12545
|
const tailSummary = !ctx.tail ? "" : KNOWN_SAFE_TAIL_SIGNATURES.has(ctx.signature) ? `; last pane output (${PANE_TAIL_PREVIEW_LINES} of ~20 lines):
|
|
12363
|
-
${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${
|
|
12546
|
+
${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash13("sha256").update(ctx.tail).digest("hex").slice(0, 12)} (raw at ~/.augmented/${codeName}/pane.log)`;
|
|
12364
12547
|
const sigSummary = ctx.signature !== "unknown" ? `; signature=${ctx.signature}` : "";
|
|
12365
12548
|
const recoverySummary = recovery ? `; recovery=${recovery}` : "";
|
|
12366
12549
|
log(
|
|
@@ -12374,7 +12557,7 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash12("sha256")
|
|
|
12374
12557
|
);
|
|
12375
12558
|
getHostId().then((hostId) => {
|
|
12376
12559
|
if (!hostId) return;
|
|
12377
|
-
const paneTailHash = zombie.paneTail ? `sha256:${
|
|
12560
|
+
const paneTailHash = zombie.paneTail ? `sha256:${createHash13("sha256").update(zombie.paneTail).digest("hex").slice(0, 12)}` : null;
|
|
12378
12561
|
return api.post("/host/events", {
|
|
12379
12562
|
host_id: hostId,
|
|
12380
12563
|
agent_code_name: codeName,
|
|
@@ -12523,7 +12706,7 @@ ${truncateForLog(ctx.tail)}` : `; pane_tail_hash=sha256:${createHash12("sha256")
|
|
|
12523
12706
|
if (!claudeAuthTupleBySession.has(codeName)) {
|
|
12524
12707
|
claudeAuthTupleBySession.set(codeName, currentAuthTuple);
|
|
12525
12708
|
}
|
|
12526
|
-
const stableTasksHash =
|
|
12709
|
+
const stableTasksHash = createHash13("sha256").update(JSON.stringify(tasks)).digest("hex").slice(0, 16);
|
|
12527
12710
|
const prevHash = agentState.knownTasksHashes.get(agent.agent_id);
|
|
12528
12711
|
if (stableTasksHash !== prevHash) {
|
|
12529
12712
|
const taskInputs = tasks.map((t) => buildSchedulerTaskInput(t));
|
|
@@ -13462,8 +13645,15 @@ function generateArtifacts(agent, refreshData, adapter) {
|
|
|
13462
13645
|
log(
|
|
13463
13646
|
`[provision/active-tasks] agent=${agent.code_name} count=${activeTasksFromRefresh.length} estimated_tokens=${activeTasksTokens}`
|
|
13464
13647
|
);
|
|
13648
|
+
const provisionAgent = (() => {
|
|
13649
|
+
const rawAgent = refreshData.agent;
|
|
13650
|
+
if (rawAgent?.["framework"] !== "opencode") return rawAgent;
|
|
13651
|
+
const hostOverride = process.env["AGT_OPENCODE_MODEL"]?.trim();
|
|
13652
|
+
const resolved = hostOverride || resolveModelChain(refreshData).primary || rawAgent?.["primary_model"];
|
|
13653
|
+
return resolved ? { ...rawAgent, primary_model: resolved } : rawAgent;
|
|
13654
|
+
})();
|
|
13465
13655
|
const provisionInput = {
|
|
13466
|
-
agent:
|
|
13656
|
+
agent: provisionAgent,
|
|
13467
13657
|
charterFrontmatter,
|
|
13468
13658
|
charterContent,
|
|
13469
13659
|
toolsFrontmatter,
|
|
@@ -13551,7 +13741,7 @@ async function syncMemories(agent, configDir, log2) {
|
|
|
13551
13741
|
if (!file.endsWith(".md")) continue;
|
|
13552
13742
|
try {
|
|
13553
13743
|
const raw = readFileSync18(join21(memoryDir, file), "utf-8");
|
|
13554
|
-
const fileHash =
|
|
13744
|
+
const fileHash = createHash13("sha256").update(raw).digest("hex").slice(0, 16);
|
|
13555
13745
|
currentHashes.set(file, fileHash);
|
|
13556
13746
|
if (prevHashes.get(file) === fileHash) continue;
|
|
13557
13747
|
const parsed = parseMemoryFile(raw, file.replace(/\.md$/, ""));
|
|
@@ -13589,14 +13779,14 @@ async function syncMemories(agent, configDir, log2) {
|
|
|
13589
13779
|
}
|
|
13590
13780
|
async function downloadMemories(agent, memoryDir, log2, { force }) {
|
|
13591
13781
|
const localFiles = existsSync12(memoryDir) ? readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).sort() : [];
|
|
13592
|
-
const localListHash =
|
|
13782
|
+
const localListHash = createHash13("sha256").update(localFiles.join(",")).digest("hex").slice(0, 16);
|
|
13593
13783
|
const prevLocalHash = lastLocalFileHash.get(agent.agent_id);
|
|
13594
13784
|
const prevDownload = lastDownloadHash.get(agent.agent_id);
|
|
13595
13785
|
try {
|
|
13596
13786
|
const dbMemories = await api.post("/host/memories", {
|
|
13597
13787
|
agent_id: agent.agent_id
|
|
13598
13788
|
});
|
|
13599
|
-
const responseHash =
|
|
13789
|
+
const responseHash = createHash13("sha256").update(JSON.stringify(dbMemories.memories ?? [])).digest("hex").slice(0, 16);
|
|
13600
13790
|
if (!force && prevDownload && prevLocalHash === localListHash && lastDownloadHash.get(agent.agent_id) === responseHash) {
|
|
13601
13791
|
return true;
|
|
13602
13792
|
}
|
|
@@ -13635,7 +13825,7 @@ ${mem.content}
|
|
|
13635
13825
|
}
|
|
13636
13826
|
if (written > 0 || overwritten > 0) {
|
|
13637
13827
|
const updatedFiles = readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
13638
|
-
lastLocalFileHash.set(agent.agent_id,
|
|
13828
|
+
lastLocalFileHash.set(agent.agent_id, createHash13("sha256").update(updatedFiles.join(",")).digest("hex").slice(0, 16));
|
|
13639
13829
|
log2(`Memory download for '${agent.code_name}': wrote ${written} new, overwrote ${overwritten} stale`);
|
|
13640
13830
|
}
|
|
13641
13831
|
}
|
|
@@ -14051,7 +14241,7 @@ function deployMcpAssets() {
|
|
|
14051
14241
|
const fileHash = (p) => {
|
|
14052
14242
|
try {
|
|
14053
14243
|
if (!existsSync12(p)) return null;
|
|
14054
|
-
return
|
|
14244
|
+
return createHash13("sha256").update(readFileSync18(p)).digest("hex");
|
|
14055
14245
|
} catch {
|
|
14056
14246
|
return null;
|
|
14057
14247
|
}
|