@integrity-labs/agt-cli 0.28.27 → 0.28.29
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 +4 -4
- package/dist/{chunk-G6KRGRHY.js → chunk-FIB2OQLF.js} +2 -2
- package/dist/{chunk-IHPN6AX7.js → chunk-SR6RHUAV.js} +2 -2
- package/dist/{chunk-SN2G4B2Z.js → chunk-VIIPFWE4.js} +254 -254
- package/dist/chunk-VIIPFWE4.js.map +1 -0
- package/dist/{claude-pair-runtime-UKOL6GWJ.js → claude-pair-runtime-RLIUZRLZ.js} +2 -2
- package/dist/lib/manager-worker.js +590 -586
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/direct-chat-channel.js +61 -17
- package/dist/{persistent-session-34CY65FC.js → persistent-session-JHBXSNVW.js} +3 -3
- package/dist/{responsiveness-probe-KKWPOZSX.js → responsiveness-probe-SKVWT5CO.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-SN2G4B2Z.js.map +0 -1
- /package/dist/{chunk-G6KRGRHY.js.map → chunk-FIB2OQLF.js.map} +0 -0
- /package/dist/{chunk-IHPN6AX7.js.map → chunk-SR6RHUAV.js.map} +0 -0
- /package/dist/{claude-pair-runtime-UKOL6GWJ.js.map → claude-pair-runtime-RLIUZRLZ.js.map} +0 -0
- /package/dist/{persistent-session-34CY65FC.js.map → persistent-session-JHBXSNVW.js.map} +0 -0
- /package/dist/{responsiveness-probe-KKWPOZSX.js.map → responsiveness-probe-SKVWT5CO.js.map} +0 -0
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
provisionStopHook,
|
|
23
23
|
requireHost,
|
|
24
24
|
safeWriteJsonAtomic
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-FIB2OQLF.js";
|
|
26
26
|
import {
|
|
27
27
|
getProjectDir as getProjectDir2,
|
|
28
28
|
getReadyTasks,
|
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
takeWatchdogGiveUpCount,
|
|
65
65
|
takeZombieDetection,
|
|
66
66
|
transcriptActivityAgeSeconds
|
|
67
|
-
} from "../chunk-
|
|
67
|
+
} from "../chunk-SR6RHUAV.js";
|
|
68
68
|
import {
|
|
69
69
|
FLAGS_SCHEMA_VERSION,
|
|
70
70
|
KANBAN_CHECK_COMMAND,
|
|
@@ -97,7 +97,7 @@ import {
|
|
|
97
97
|
sumTranscriptUsageInWindow,
|
|
98
98
|
worseConnectivityOutcome,
|
|
99
99
|
wrapScheduledTaskPrompt
|
|
100
|
-
} from "../chunk-
|
|
100
|
+
} from "../chunk-VIIPFWE4.js";
|
|
101
101
|
import {
|
|
102
102
|
parsePsRows,
|
|
103
103
|
reapOrphanChannelMcps
|
|
@@ -3416,63 +3416,6 @@ function killAgentChannelProcesses(codeName, opts) {
|
|
|
3416
3416
|
return pids;
|
|
3417
3417
|
}
|
|
3418
3418
|
|
|
3419
|
-
// src/lib/delivery-schedule-link.ts
|
|
3420
|
-
function envSuffixFor(codeName) {
|
|
3421
|
-
return codeName.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
3422
|
-
}
|
|
3423
|
-
function scheduleLinkFooterEnabled(codeName, env = process.env) {
|
|
3424
|
-
if (codeName) {
|
|
3425
|
-
const perAgent = env[`AGT_SCHEDULE_LINK_FOOTER_DISABLED__${envSuffixFor(codeName)}`];
|
|
3426
|
-
if (perAgent === "1") return false;
|
|
3427
|
-
}
|
|
3428
|
-
if (env["AGT_SCHEDULE_LINK_FOOTER_DISABLED"] === "1") return false;
|
|
3429
|
-
return true;
|
|
3430
|
-
}
|
|
3431
|
-
function getConsoleUrl(env = process.env) {
|
|
3432
|
-
const canonical = env["AGT_CONSOLE_URL"]?.trim();
|
|
3433
|
-
if (canonical) return canonical.replace(/\/+$/, "");
|
|
3434
|
-
const fallback = env["NEXT_PUBLIC_APP_URL"]?.trim();
|
|
3435
|
-
if (fallback) return fallback.replace(/\/+$/, "");
|
|
3436
|
-
return null;
|
|
3437
|
-
}
|
|
3438
|
-
function buildScheduleEditLink(consoleUrl, agentId, taskId) {
|
|
3439
|
-
const base = consoleUrl.replace(/\/+$/, "");
|
|
3440
|
-
return `${base}/agents/${encodeURIComponent(agentId)}/schedules/${encodeURIComponent(taskId)}`;
|
|
3441
|
-
}
|
|
3442
|
-
function formatScheduleLinkFooter(url, medium) {
|
|
3443
|
-
if (medium === "slack") return `<${url}|Edit schedule>`;
|
|
3444
|
-
if (medium === "telegram") return `Edit schedule: ${url}`;
|
|
3445
|
-
return url;
|
|
3446
|
-
}
|
|
3447
|
-
function appendScheduleLinkFooter(body, url, medium) {
|
|
3448
|
-
const footer = formatScheduleLinkFooter(url, medium);
|
|
3449
|
-
const trimmed = body.replace(/\s+$/, "");
|
|
3450
|
-
if (trimmed.endsWith(footer)) return trimmed;
|
|
3451
|
-
return `${trimmed}
|
|
3452
|
-
|
|
3453
|
-
${footer}`;
|
|
3454
|
-
}
|
|
3455
|
-
var warnedNullConsoleUrl = false;
|
|
3456
|
-
function withScheduleLinkFooter(opts) {
|
|
3457
|
-
if (!opts.taskId) return opts.body;
|
|
3458
|
-
if (!scheduleLinkFooterEnabled(opts.codeName, opts.env)) return opts.body;
|
|
3459
|
-
const consoleUrl = getConsoleUrl(opts.env);
|
|
3460
|
-
if (!consoleUrl) {
|
|
3461
|
-
if (!warnedNullConsoleUrl && opts.log) {
|
|
3462
|
-
warnedNullConsoleUrl = true;
|
|
3463
|
-
try {
|
|
3464
|
-
opts.log(
|
|
3465
|
-
"[schedule-link] AGT_CONSOLE_URL unset and NEXT_PUBLIC_APP_URL unset \u2014 schedule-edit deep-link footer disabled. Run `agt setup` again or export AGT_CONSOLE_URL (e.g. https://app.augmented.team) to restore it."
|
|
3466
|
-
);
|
|
3467
|
-
} catch {
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
return opts.body;
|
|
3471
|
-
}
|
|
3472
|
-
const url = buildScheduleEditLink(consoleUrl, opts.agentId, opts.taskId);
|
|
3473
|
-
return appendScheduleLinkFooter(opts.body, url, opts.medium);
|
|
3474
|
-
}
|
|
3475
|
-
|
|
3476
3419
|
// src/lib/manager/agent-state.ts
|
|
3477
3420
|
var agentState = {
|
|
3478
3421
|
// ---------------------------------------------------------------------------
|
|
@@ -4065,11 +4008,11 @@ function setAlertSlackWebhook(value) {
|
|
|
4065
4008
|
|
|
4066
4009
|
// src/lib/delivery-hint.ts
|
|
4067
4010
|
var DEFAULT_PROBABILITY = 0.1;
|
|
4068
|
-
function
|
|
4011
|
+
function envSuffixFor(codeName) {
|
|
4069
4012
|
return codeName.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
4070
4013
|
}
|
|
4071
4014
|
function hintProbability(codeName, env = process.env) {
|
|
4072
|
-
const suffix = codeName ? `__${
|
|
4015
|
+
const suffix = codeName ? `__${envSuffixFor(codeName)}` : "";
|
|
4073
4016
|
if (suffix && env[`AGT_DELIVERY_HINT_DISABLED${suffix}`] === "1") return 0;
|
|
4074
4017
|
const perAgent = suffix ? env[`AGT_DELIVERY_HINT_PROBABILITY${suffix}`] : void 0;
|
|
4075
4018
|
if (perAgent != null) return clampProbability(perAgent);
|
|
@@ -4236,139 +4179,505 @@ async function maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId) {
|
|
|
4236
4179
|
}
|
|
4237
4180
|
}
|
|
4238
4181
|
|
|
4239
|
-
// src/lib/
|
|
4240
|
-
var
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
inboundHardWaitSeconds: 0,
|
|
4254
|
-
paneStaleSeconds: 120,
|
|
4255
|
-
transcriptStaleSeconds: 60,
|
|
4256
|
-
minCycles: 3
|
|
4257
|
-
};
|
|
4258
|
-
function parseMode(raw) {
|
|
4259
|
-
const v = (raw ?? "").trim().toLowerCase();
|
|
4260
|
-
return v === "shadow" || v === "enforce" ? v : "off";
|
|
4261
|
-
}
|
|
4262
|
-
function parsePositiveInt(raw, fallback, floor) {
|
|
4263
|
-
const n = raw ? Number.parseInt(raw, 10) : NaN;
|
|
4264
|
-
return Number.isInteger(n) && n >= floor ? n : fallback;
|
|
4265
|
-
}
|
|
4266
|
-
function resolveWedgeConfig(env = process.env) {
|
|
4267
|
-
const inboundWaitSeconds = parsePositiveInt(
|
|
4268
|
-
env.AGT_WEDGE_INBOUND_WAIT_SECONDS,
|
|
4269
|
-
DEFAULTS.inboundWaitSeconds,
|
|
4270
|
-
30
|
|
4271
|
-
);
|
|
4272
|
-
const inboundHardWaitRaw = parsePositiveInt(
|
|
4273
|
-
env.AGT_WEDGE_INBOUND_HARD_WAIT_SECONDS,
|
|
4274
|
-
DEFAULTS.inboundHardWaitSeconds,
|
|
4275
|
-
0
|
|
4276
|
-
);
|
|
4277
|
-
const inboundHardWaitSeconds = inboundHardWaitRaw <= 0 ? 0 : Math.max(inboundWaitSeconds, inboundHardWaitRaw);
|
|
4278
|
-
return {
|
|
4279
|
-
mode: parseMode(env.AGT_WEDGE_RESTART_MODE),
|
|
4280
|
-
inboundWaitSeconds,
|
|
4281
|
-
inboundHardWaitSeconds,
|
|
4282
|
-
paneStaleSeconds: parsePositiveInt(env.AGT_WEDGE_PANE_STALE_SECONDS, DEFAULTS.paneStaleSeconds, 30),
|
|
4283
|
-
transcriptStaleSeconds: parsePositiveInt(
|
|
4284
|
-
env.AGT_WEDGE_TRANSCRIPT_STALE_SECONDS,
|
|
4285
|
-
DEFAULTS.transcriptStaleSeconds,
|
|
4286
|
-
15
|
|
4287
|
-
),
|
|
4288
|
-
minCycles: parsePositiveInt(env.AGT_WEDGE_MIN_CYCLES, DEFAULTS.minCycles, 2)
|
|
4289
|
-
};
|
|
4290
|
-
}
|
|
4291
|
-
function isSessionProducing(signals, config2) {
|
|
4292
|
-
const subagentAge = signals.subagentActivityAgeSeconds;
|
|
4293
|
-
if (subagentAge !== null && subagentAge < config2.transcriptStaleSeconds) return true;
|
|
4294
|
-
const transcriptAge = signals.transcriptActivityAgeSeconds;
|
|
4295
|
-
if (transcriptAge !== null) return transcriptAge < config2.transcriptStaleSeconds;
|
|
4296
|
-
const paneAge = signals.paneActivityAgeSeconds;
|
|
4297
|
-
return paneAge !== null && paneAge < config2.paneStaleSeconds;
|
|
4298
|
-
}
|
|
4299
|
-
function wedgeExemptionReason(signals, config2) {
|
|
4300
|
-
if (signals.subagentActivityAgeSeconds === null) return null;
|
|
4301
|
-
if (isWedgeCandidateCycle(signals, config2)) return null;
|
|
4302
|
-
const withoutSubagent = { ...signals, subagentActivityAgeSeconds: null };
|
|
4303
|
-
return isWedgeCandidateCycle(withoutSubagent, config2) ? "background-task-in-flight" : null;
|
|
4304
|
-
}
|
|
4305
|
-
function isWedgeCandidateCycle(signals, config2) {
|
|
4306
|
-
const inboundAge = signals.pendingInboundOldestAgeSeconds;
|
|
4307
|
-
if (inboundAge === null) return false;
|
|
4308
|
-
if (inboundAge < config2.inboundWaitSeconds) return false;
|
|
4309
|
-
if (isSessionProducing(signals, config2)) {
|
|
4310
|
-
if (config2.inboundHardWaitSeconds <= 0) return false;
|
|
4311
|
-
return inboundAge >= config2.inboundHardWaitSeconds;
|
|
4182
|
+
// src/lib/manager/delivery/state.ts
|
|
4183
|
+
var agentInfoForDelivery = /* @__PURE__ */ new Map();
|
|
4184
|
+
function cacheAgentDeliveryMetadata(codeName, refreshData) {
|
|
4185
|
+
const agentRow = refreshData["agent"] ?? {};
|
|
4186
|
+
const displayName = agentRow["display_name"] ?? codeName;
|
|
4187
|
+
const ownerTeamName = agentRow["owner_team_name"] ?? null;
|
|
4188
|
+
const framework = agentRow["framework"] ?? "openclaw";
|
|
4189
|
+
const reportsToPersonId = agentRow["reports_to"] ?? null;
|
|
4190
|
+
const reportsToType = agentRow["reports_to_type"] ?? null;
|
|
4191
|
+
const channelConfigs = refreshData["channel_configs"] ?? {};
|
|
4192
|
+
const dmCapable = [];
|
|
4193
|
+
const slackBotToken = channelConfigs["slack"]?.config?.["bot_token"];
|
|
4194
|
+
if (typeof slackBotToken === "string" && slackBotToken.length > 0) {
|
|
4195
|
+
dmCapable.push("slack");
|
|
4312
4196
|
}
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
cooldownSeconds: 1800
|
|
4324
|
-
// 30 min — matches the kanban stale-auto-fail window
|
|
4325
|
-
};
|
|
4326
|
-
function parsePositiveInt2(raw, fallback, floor) {
|
|
4327
|
-
const n = raw ? Number.parseInt(raw, 10) : NaN;
|
|
4328
|
-
return Number.isInteger(n) && n >= floor ? n : fallback;
|
|
4329
|
-
}
|
|
4330
|
-
function resolvePoisonCardConfig(env = process.env) {
|
|
4331
|
-
return {
|
|
4332
|
-
threshold: parsePositiveInt2(env.AGT_WEDGE_POISON_CARD_THRESHOLD, DEFAULTS2.threshold, 2),
|
|
4333
|
-
cooldownMs: parsePositiveInt2(
|
|
4334
|
-
env.AGT_WEDGE_POISON_CARD_COOLDOWN_SECONDS,
|
|
4335
|
-
DEFAULTS2.cooldownSeconds,
|
|
4336
|
-
300
|
|
4337
|
-
) * 1e3
|
|
4197
|
+
const telegramBotToken = channelConfigs["telegram"]?.config?.["bot_token"];
|
|
4198
|
+
if (typeof telegramBotToken === "string" && telegramBotToken.length > 0) {
|
|
4199
|
+
dmCapable.push("telegram");
|
|
4200
|
+
}
|
|
4201
|
+
const resolverAgent = {
|
|
4202
|
+
agent_id: agentRow["agent_id"] ?? "",
|
|
4203
|
+
framework,
|
|
4204
|
+
dm_capable_mediums: dmCapable,
|
|
4205
|
+
reports_to_person_id: reportsToType === "person" ? reportsToPersonId : null,
|
|
4206
|
+
reports_to_type: reportsToType
|
|
4338
4207
|
};
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4208
|
+
const peopleByPersonId = /* @__PURE__ */ new Map();
|
|
4209
|
+
if (reportsToPersonId && reportsToType === "person") {
|
|
4210
|
+
peopleByPersonId.set(reportsToPersonId, {
|
|
4211
|
+
person_id: reportsToPersonId,
|
|
4212
|
+
display_name: agentRow["reports_to_name"] ?? "Reports-To",
|
|
4213
|
+
slack_user_id: agentRow["reports_to_slack_user_id"] ?? null,
|
|
4214
|
+
telegram_chat_id: agentRow["reports_to_telegram_chat_id"] ?? null
|
|
4215
|
+
});
|
|
4343
4216
|
}
|
|
4344
|
-
const
|
|
4345
|
-
const
|
|
4346
|
-
|
|
4347
|
-
if (
|
|
4348
|
-
const
|
|
4349
|
-
|
|
4350
|
-
|
|
4217
|
+
const people = refreshData["people"] ?? [];
|
|
4218
|
+
for (const p of people) {
|
|
4219
|
+
const personId = p["person_id"];
|
|
4220
|
+
if (!personId) continue;
|
|
4221
|
+
const contactPrefs = p["contact_preferences"] ?? {};
|
|
4222
|
+
peopleByPersonId.set(personId, {
|
|
4223
|
+
person_id: personId,
|
|
4224
|
+
display_name: p["display_name"] ?? "person",
|
|
4225
|
+
slack_user_id: contactPrefs["slack_user_id"] ?? null,
|
|
4226
|
+
telegram_chat_id: contactPrefs["telegram_chat_id"] ?? null
|
|
4227
|
+
});
|
|
4351
4228
|
}
|
|
4352
|
-
|
|
4229
|
+
agentInfoForDelivery.set(codeName, {
|
|
4230
|
+
agentDisplayName: displayName,
|
|
4231
|
+
ownerTeamName,
|
|
4232
|
+
resolverAgent,
|
|
4233
|
+
peopleByPersonId
|
|
4234
|
+
});
|
|
4353
4235
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4236
|
+
|
|
4237
|
+
// src/lib/manager/delivery/notifications.ts
|
|
4238
|
+
var NOTIFY_RESULT_MAX_CHARS = 3500;
|
|
4239
|
+
async function reportDeliveryStatus(agentId, taskId, payload) {
|
|
4240
|
+
if (!taskId) return;
|
|
4241
|
+
try {
|
|
4242
|
+
await api.post("/host/schedules/delivery-status", {
|
|
4243
|
+
agent_id: agentId,
|
|
4244
|
+
task_id: taskId,
|
|
4245
|
+
status: payload.status,
|
|
4246
|
+
medium: payload.medium ?? null,
|
|
4247
|
+
error_code: payload.error_code ?? null
|
|
4248
|
+
});
|
|
4249
|
+
} catch (err) {
|
|
4250
|
+
log(`[delivery] Failed to report delivery status for ${agentId}/${taskId}: ${err.message}`);
|
|
4361
4251
|
}
|
|
4362
|
-
return next;
|
|
4363
4252
|
}
|
|
4364
|
-
function
|
|
4365
|
-
|
|
4253
|
+
async function maybePostScheduledTaskRatingPrompt(agentId, taskId, channelId, messageTs) {
|
|
4254
|
+
try {
|
|
4255
|
+
await api.post("/host/scheduled-task/rating-prompt", {
|
|
4256
|
+
agent_id: agentId,
|
|
4257
|
+
task_id: taskId,
|
|
4258
|
+
channel: channelId,
|
|
4259
|
+
message_ts: messageTs
|
|
4260
|
+
});
|
|
4261
|
+
} catch (err) {
|
|
4262
|
+
log(`[rating-prompt] Failed to post rating prompt for ${agentId}/${taskId}: ${err.message}`);
|
|
4263
|
+
}
|
|
4366
4264
|
}
|
|
4367
|
-
function
|
|
4368
|
-
const
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4265
|
+
async function buildDoneCardNotification(agentId, codeName, displayName, item) {
|
|
4266
|
+
const isFailed = item.status === "failed";
|
|
4267
|
+
let resultBody = item.result ?? "";
|
|
4268
|
+
try {
|
|
4269
|
+
const full = await api.post("/host/kanban-item", {
|
|
4270
|
+
agent_id: agentId,
|
|
4271
|
+
item_id: item.id
|
|
4272
|
+
});
|
|
4273
|
+
if (full.item?.result) resultBody = full.item.result;
|
|
4274
|
+
} catch (err) {
|
|
4275
|
+
log(`[notify] full-result fetch failed for card ${item.id} on '${codeName}': ${err.message} \u2014 using truncated board result`);
|
|
4276
|
+
}
|
|
4277
|
+
const resultLine = resultBody ? `
|
|
4278
|
+
${isFailed ? "Reason" : "Result"}: ${resultBody.slice(0, NOTIFY_RESULT_MAX_CHARS)}` : "";
|
|
4279
|
+
const emoji = isFailed ? "\u274C" : "\u2705";
|
|
4280
|
+
return `${emoji} ${isFailed ? "Task Failed" : "Task Complete"} \u2014 ${displayName}
|
|
4281
|
+
${item.title}${resultLine}`;
|
|
4282
|
+
}
|
|
4283
|
+
async function sendTaskNotification(agentCodeName, channel, to, text) {
|
|
4284
|
+
const tokens = agentChannelTokens.get(agentCodeName);
|
|
4285
|
+
if (channel === "slack") {
|
|
4286
|
+
const botToken = tokens?.slack;
|
|
4287
|
+
const channelId = to.replace(/^channel:/, "");
|
|
4288
|
+
if (!botToken) {
|
|
4289
|
+
log(`No Slack bot token for '${agentCodeName}' \u2014 targeted notification dropped`);
|
|
4290
|
+
return { ok: false, error_code: "SLACK_NO_TOKEN" };
|
|
4291
|
+
}
|
|
4292
|
+
const sent = await sendSlackChannelMessage(agentCodeName, channelId, text);
|
|
4293
|
+
return sent ? { ok: true } : { ok: false, error_code: "SLACK_SEND_FAILED" };
|
|
4294
|
+
}
|
|
4295
|
+
if (channel === "telegram") {
|
|
4296
|
+
const botToken = tokens?.telegram;
|
|
4297
|
+
const chatId = to.replace(/^chat:/, "");
|
|
4298
|
+
if (!botToken) {
|
|
4299
|
+
log(`No Telegram bot token for '${agentCodeName}' \u2014 notification dropped`);
|
|
4300
|
+
return { ok: false, error_code: "TELEGRAM_NO_TOKEN" };
|
|
4301
|
+
}
|
|
4302
|
+
const allowedChats = tokens?.telegramAllowedChats;
|
|
4303
|
+
if (allowedChats && allowedChats.length > 0 && !allowedChats.includes(chatId)) {
|
|
4304
|
+
log(`Telegram chat ${chatId} not in allowed_chat_ids for '${agentCodeName}'`);
|
|
4305
|
+
return { ok: false, error_code: "TELEGRAM_CHAT_NOT_ALLOWED" };
|
|
4306
|
+
}
|
|
4307
|
+
try {
|
|
4308
|
+
const result = await telegramApiCall(botToken, "sendMessage", { chat_id: chatId, text });
|
|
4309
|
+
if (!result.ok) {
|
|
4310
|
+
log(`Telegram sendMessage failed for '${agentCodeName}': ${result.description}`);
|
|
4311
|
+
return { ok: false, error_code: `TELEGRAM_SEND_FAILED:${result.description ?? "unknown"}` };
|
|
4312
|
+
}
|
|
4313
|
+
log(`Telegram notification sent for '${agentCodeName}' to chat ${chatId}`);
|
|
4314
|
+
return { ok: true };
|
|
4315
|
+
} catch (err) {
|
|
4316
|
+
log(`Telegram API error for '${agentCodeName}': ${err.message}`);
|
|
4317
|
+
return { ok: false, error_code: "TELEGRAM_EXCEPTION" };
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
log(`Unknown notify_channel '${channel}' for '${agentCodeName}'`);
|
|
4321
|
+
return { ok: false, error_code: `UNKNOWN_CHANNEL:${channel}` };
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
// src/lib/delivery-schedule-link.ts
|
|
4325
|
+
function envSuffixFor2(codeName) {
|
|
4326
|
+
return codeName.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
4327
|
+
}
|
|
4328
|
+
function scheduleLinkFooterEnabled(codeName, env = process.env) {
|
|
4329
|
+
if (codeName) {
|
|
4330
|
+
const perAgent = env[`AGT_SCHEDULE_LINK_FOOTER_DISABLED__${envSuffixFor2(codeName)}`];
|
|
4331
|
+
if (perAgent === "1") return false;
|
|
4332
|
+
}
|
|
4333
|
+
if (env["AGT_SCHEDULE_LINK_FOOTER_DISABLED"] === "1") return false;
|
|
4334
|
+
return true;
|
|
4335
|
+
}
|
|
4336
|
+
function getConsoleUrl(env = process.env) {
|
|
4337
|
+
const canonical = env["AGT_CONSOLE_URL"]?.trim();
|
|
4338
|
+
if (canonical) return canonical.replace(/\/+$/, "");
|
|
4339
|
+
const fallback = env["NEXT_PUBLIC_APP_URL"]?.trim();
|
|
4340
|
+
if (fallback) return fallback.replace(/\/+$/, "");
|
|
4341
|
+
return null;
|
|
4342
|
+
}
|
|
4343
|
+
function buildScheduleEditLink(consoleUrl, agentId, taskId) {
|
|
4344
|
+
const base = consoleUrl.replace(/\/+$/, "");
|
|
4345
|
+
return `${base}/agents/${encodeURIComponent(agentId)}/schedules/${encodeURIComponent(taskId)}`;
|
|
4346
|
+
}
|
|
4347
|
+
function formatScheduleLinkFooter(url, medium) {
|
|
4348
|
+
if (medium === "slack") return `<${url}|Edit schedule>`;
|
|
4349
|
+
if (medium === "telegram") return `Edit schedule: ${url}`;
|
|
4350
|
+
return url;
|
|
4351
|
+
}
|
|
4352
|
+
function appendScheduleLinkFooter(body, url, medium) {
|
|
4353
|
+
const footer = formatScheduleLinkFooter(url, medium);
|
|
4354
|
+
const trimmed = body.replace(/\s+$/, "");
|
|
4355
|
+
if (trimmed.endsWith(footer)) return trimmed;
|
|
4356
|
+
return `${trimmed}
|
|
4357
|
+
|
|
4358
|
+
${footer}`;
|
|
4359
|
+
}
|
|
4360
|
+
var warnedNullConsoleUrl = false;
|
|
4361
|
+
function withScheduleLinkFooter(opts) {
|
|
4362
|
+
if (!opts.taskId) return opts.body;
|
|
4363
|
+
if (!scheduleLinkFooterEnabled(opts.codeName, opts.env)) return opts.body;
|
|
4364
|
+
const consoleUrl = getConsoleUrl(opts.env);
|
|
4365
|
+
if (!consoleUrl) {
|
|
4366
|
+
if (!warnedNullConsoleUrl && opts.log) {
|
|
4367
|
+
warnedNullConsoleUrl = true;
|
|
4368
|
+
try {
|
|
4369
|
+
opts.log(
|
|
4370
|
+
"[schedule-link] AGT_CONSOLE_URL unset and NEXT_PUBLIC_APP_URL unset \u2014 schedule-edit deep-link footer disabled. Run `agt setup` again or export AGT_CONSOLE_URL (e.g. https://app.augmented.team) to restore it."
|
|
4371
|
+
);
|
|
4372
|
+
} catch {
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
return opts.body;
|
|
4376
|
+
}
|
|
4377
|
+
const url = buildScheduleEditLink(consoleUrl, opts.agentId, opts.taskId);
|
|
4378
|
+
return appendScheduleLinkFooter(opts.body, url, opts.medium);
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
// src/lib/manager/delivery/scheduled-output.ts
|
|
4382
|
+
async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, body, taskId) {
|
|
4383
|
+
const withLink = (b, medium) => withScheduleLinkFooter({ body: b, medium, codeName: agentCodeName, agentId, taskId, log });
|
|
4384
|
+
if (typeof rawTarget === "string") {
|
|
4385
|
+
if (rawTarget.startsWith("channel:")) {
|
|
4386
|
+
const result = await sendTaskNotification(agentCodeName, "slack", rawTarget, withLink(body, "slack"));
|
|
4387
|
+
await reportDeliveryStatus(agentId, taskId, {
|
|
4388
|
+
status: result.ok ? "ok" : "failed",
|
|
4389
|
+
medium: "slack",
|
|
4390
|
+
error_code: result.ok ? null : result.error_code ?? "SLACK_SEND_FAILED"
|
|
4391
|
+
});
|
|
4392
|
+
return;
|
|
4393
|
+
}
|
|
4394
|
+
if (rawTarget.startsWith("chat:")) {
|
|
4395
|
+
const result = await sendTaskNotification(agentCodeName, "telegram", rawTarget, withLink(body, "telegram"));
|
|
4396
|
+
await reportDeliveryStatus(agentId, taskId, {
|
|
4397
|
+
status: result.ok ? "ok" : "failed",
|
|
4398
|
+
medium: "telegram",
|
|
4399
|
+
error_code: result.ok ? null : result.error_code ?? "TELEGRAM_SEND_FAILED"
|
|
4400
|
+
});
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4403
|
+
log(`[delivery] Unrecognised legacy delivery_to string for '${agentCodeName}': ${rawTarget.slice(0, 60)}`);
|
|
4404
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "LEGACY_DELIVERY_TO_UNRECOGNISED" });
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
const parsed = parseDeliveryTarget(rawTarget);
|
|
4408
|
+
if (isParseError(parsed)) {
|
|
4409
|
+
log(`[delivery] Malformed delivery_to for '${agentCodeName}': ${parsed.code} \u2014 ${parsed.detail}`);
|
|
4410
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: parsed.code });
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4413
|
+
if (parsed.kind === "channel") {
|
|
4414
|
+
if (parsed.provider === "slack") {
|
|
4415
|
+
const channelId = parsed.channel_id ?? "";
|
|
4416
|
+
const threadTs = parsed.thread_ts;
|
|
4417
|
+
let sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"), threadTs);
|
|
4418
|
+
let deliveredInThread = Boolean(threadTs);
|
|
4419
|
+
if (!sent.ok && threadTs && (sent.error === "thread_not_found" || sent.error === "message_not_found")) {
|
|
4420
|
+
log(`[delivery] Originating thread ${threadTs} gone in ${channelId} for '${agentCodeName}' \u2014 falling back to top-level post`);
|
|
4421
|
+
sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"));
|
|
4422
|
+
deliveredInThread = false;
|
|
4423
|
+
}
|
|
4424
|
+
await reportDeliveryStatus(agentId, taskId, {
|
|
4425
|
+
status: sent.ok ? "ok" : "failed",
|
|
4426
|
+
medium: "slack",
|
|
4427
|
+
// Preserve Slack's concrete failure code (channel_not_found,
|
|
4428
|
+
// not_in_channel, auth errors, …) in the observability row rather
|
|
4429
|
+
// than collapsing everything to the generic constant.
|
|
4430
|
+
error_code: sent.ok ? null : sent.error ? `SLACK_SEND_FAILED:${sent.error}` : "SLACK_SEND_FAILED"
|
|
4431
|
+
});
|
|
4432
|
+
if (sent.ok) {
|
|
4433
|
+
if (!deliveredInThread) {
|
|
4434
|
+
await maybePostSlackThreadHint(agentCodeName, channelId, sent.ts);
|
|
4435
|
+
}
|
|
4436
|
+
if (sent.ts && taskId) {
|
|
4437
|
+
await maybePostScheduledTaskRatingPrompt(agentId, taskId, channelId, sent.ts);
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4442
|
+
const chatId = parsed.chat_id ?? "";
|
|
4443
|
+
const toStr = `chat:${chatId}`;
|
|
4444
|
+
const result = await sendTaskNotification(agentCodeName, "telegram", toStr, withLink(body, "telegram"));
|
|
4445
|
+
await reportDeliveryStatus(agentId, taskId, {
|
|
4446
|
+
status: result.ok ? "ok" : "failed",
|
|
4447
|
+
medium: "telegram",
|
|
4448
|
+
error_code: result.ok ? null : result.error_code ?? "TELEGRAM_SEND_FAILED"
|
|
4449
|
+
});
|
|
4450
|
+
if (result.ok) {
|
|
4451
|
+
const botToken = agentChannelTokens.get(agentCodeName)?.telegram;
|
|
4452
|
+
if (botToken) await maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId);
|
|
4453
|
+
}
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
const agentRow = agentInfoForDelivery.get(agentCodeName);
|
|
4457
|
+
if (!agentRow) {
|
|
4458
|
+
log(`[delivery] No agent metadata cached for '${agentCodeName}' \u2014 dropping DM`);
|
|
4459
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "AGENT_METADATA_UNAVAILABLE" });
|
|
4460
|
+
return;
|
|
4461
|
+
}
|
|
4462
|
+
const resolved = resolveDmTarget(parsed, agentRow.resolverAgent, agentRow.peopleByPersonId);
|
|
4463
|
+
if (isResolveError(resolved)) {
|
|
4464
|
+
log(`[delivery] Cannot resolve DM target for '${agentCodeName}': ${resolved.code} \u2014 ${resolved.detail}`);
|
|
4465
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: resolved.code });
|
|
4466
|
+
return;
|
|
4467
|
+
}
|
|
4468
|
+
const attributionBody = appendDmFooter(
|
|
4469
|
+
body,
|
|
4470
|
+
agentRow.ownerTeamName,
|
|
4471
|
+
agentRow.agentDisplayName
|
|
4472
|
+
);
|
|
4473
|
+
const footeredBody = resolved.kind === "dm" ? withLink(attributionBody, resolved.medium === "slack" ? "slack" : "telegram") : attributionBody;
|
|
4474
|
+
if (resolved.kind === "dm" && resolved.medium === "slack") {
|
|
4475
|
+
const tokens = agentChannelTokens.get(agentCodeName);
|
|
4476
|
+
const botToken = tokens?.slack;
|
|
4477
|
+
if (!botToken) {
|
|
4478
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "SLACK_MISSING_SCOPE", medium: "slack" });
|
|
4479
|
+
log(`[delivery] No Slack bot token for '${agentCodeName}' \u2014 DM dropped`);
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
try {
|
|
4483
|
+
const controller = new AbortController();
|
|
4484
|
+
const timeoutHandle = setTimeout(() => controller.abort(), 5e3);
|
|
4485
|
+
let openJson;
|
|
4486
|
+
try {
|
|
4487
|
+
const openResp = await fetch("https://slack.com/api/conversations.open", {
|
|
4488
|
+
method: "POST",
|
|
4489
|
+
headers: {
|
|
4490
|
+
Authorization: `Bearer ${botToken}`,
|
|
4491
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
4492
|
+
},
|
|
4493
|
+
body: JSON.stringify({ users: resolved.slack_user_id }),
|
|
4494
|
+
signal: controller.signal
|
|
4495
|
+
});
|
|
4496
|
+
openJson = await openResp.json();
|
|
4497
|
+
} finally {
|
|
4498
|
+
clearTimeout(timeoutHandle);
|
|
4499
|
+
}
|
|
4500
|
+
if (!openJson.ok || !openJson.channel?.id) {
|
|
4501
|
+
const errCode = openJson.error === "missing_scope" ? "SLACK_MISSING_SCOPE" : `SLACK_OPEN_FAILED:${openJson.error ?? "unknown"}`;
|
|
4502
|
+
log(`[delivery] conversations.open failed for '${agentCodeName}': ${openJson.error}`);
|
|
4503
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: errCode, medium: "slack" });
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
const sent = await postSlackChannelMessage(agentCodeName, openJson.channel.id, footeredBody);
|
|
4507
|
+
await reportDeliveryStatus(agentId, taskId, {
|
|
4508
|
+
status: sent.ok ? "ok" : "failed",
|
|
4509
|
+
medium: "slack",
|
|
4510
|
+
error_code: sent.ok ? null : "SLACK_SEND_FAILED"
|
|
4511
|
+
});
|
|
4512
|
+
if (sent.ok) await maybePostSlackThreadHint(agentCodeName, openJson.channel.id, sent.ts);
|
|
4513
|
+
} catch (err) {
|
|
4514
|
+
const isAbort = err.name === "AbortError";
|
|
4515
|
+
const errCode = isAbort ? "SLACK_OPEN_TIMEOUT" : "SLACK_EXCEPTION";
|
|
4516
|
+
log(`[delivery] Slack DM ${isAbort ? "timeout" : "failure"} for '${agentCodeName}': ${err.message}`);
|
|
4517
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: errCode, medium: "slack" });
|
|
4518
|
+
}
|
|
4519
|
+
return;
|
|
4520
|
+
}
|
|
4521
|
+
if (resolved.kind === "dm" && resolved.medium === "telegram") {
|
|
4522
|
+
const tokens = agentChannelTokens.get(agentCodeName);
|
|
4523
|
+
const botToken = tokens?.telegram;
|
|
4524
|
+
if (!botToken) {
|
|
4525
|
+
log(`[delivery] No Telegram bot token for '${agentCodeName}' \u2014 DM dropped`);
|
|
4526
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "TELEGRAM_NO_TOKEN", medium: "telegram" });
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
try {
|
|
4530
|
+
const result = await telegramApiCall(botToken, "sendMessage", {
|
|
4531
|
+
chat_id: resolved.telegram_chat_id,
|
|
4532
|
+
text: footeredBody
|
|
4533
|
+
});
|
|
4534
|
+
if (!result.ok) {
|
|
4535
|
+
log(`[delivery] Telegram DM failed for '${agentCodeName}': ${result.description}`);
|
|
4536
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: `TELEGRAM_SEND_FAILED:${result.description ?? "unknown"}`, medium: "telegram" });
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4539
|
+
await reportDeliveryStatus(agentId, taskId, { status: "ok", medium: "telegram" });
|
|
4540
|
+
await maybeSendTelegramFollowUpHint(agentCodeName, botToken, resolved.telegram_chat_id);
|
|
4541
|
+
} catch (err) {
|
|
4542
|
+
log(`[delivery] Telegram DM exception for '${agentCodeName}': ${err.message}`);
|
|
4543
|
+
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "TELEGRAM_EXCEPTION", medium: "telegram" });
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
|
|
4548
|
+
// src/lib/wedge-detection.ts
|
|
4549
|
+
var DEFAULTS = {
|
|
4550
|
+
inboundWaitSeconds: 120,
|
|
4551
|
+
// ENG-6264: DISABLED by default (0). A session that's actively producing
|
|
4552
|
+
// tokens is never force-respawned — a working agent must not be killed just
|
|
4553
|
+
// because a message has been queued behind its turn, no matter how long.
|
|
4554
|
+
// ENG-6238 made this an absolute backstop (1200s) to still catch a
|
|
4555
|
+
// producing-but-never-draining runaway loop, but that re-introduced the exact
|
|
4556
|
+
// failure we set out to kill: cutting off real work on a long turn. Runaway
|
|
4557
|
+
// token burn is owned by the cost guardrail (ENG-5556); a producing-but-silent
|
|
4558
|
+
// loop still trips the synthetic-probe alarm. So the backstop is now opt-in:
|
|
4559
|
+
// set AGT_WEDGE_INBOUND_HARD_WAIT_SECONDS to a positive value to re-enable it
|
|
4560
|
+
// (floored at inboundWaitSeconds). 0 = the frozen/hung wedge (transcript
|
|
4561
|
+
// static) is still caught by the soft path; only the *producing* path is spared.
|
|
4562
|
+
inboundHardWaitSeconds: 0,
|
|
4563
|
+
paneStaleSeconds: 120,
|
|
4564
|
+
transcriptStaleSeconds: 60,
|
|
4565
|
+
minCycles: 3
|
|
4566
|
+
};
|
|
4567
|
+
function parseMode(raw) {
|
|
4568
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
4569
|
+
return v === "shadow" || v === "enforce" ? v : "off";
|
|
4570
|
+
}
|
|
4571
|
+
function parsePositiveInt(raw, fallback, floor) {
|
|
4572
|
+
const n = raw ? Number.parseInt(raw, 10) : NaN;
|
|
4573
|
+
return Number.isInteger(n) && n >= floor ? n : fallback;
|
|
4574
|
+
}
|
|
4575
|
+
function resolveWedgeConfig(env = process.env) {
|
|
4576
|
+
const inboundWaitSeconds = parsePositiveInt(
|
|
4577
|
+
env.AGT_WEDGE_INBOUND_WAIT_SECONDS,
|
|
4578
|
+
DEFAULTS.inboundWaitSeconds,
|
|
4579
|
+
30
|
|
4580
|
+
);
|
|
4581
|
+
const inboundHardWaitRaw = parsePositiveInt(
|
|
4582
|
+
env.AGT_WEDGE_INBOUND_HARD_WAIT_SECONDS,
|
|
4583
|
+
DEFAULTS.inboundHardWaitSeconds,
|
|
4584
|
+
0
|
|
4585
|
+
);
|
|
4586
|
+
const inboundHardWaitSeconds = inboundHardWaitRaw <= 0 ? 0 : Math.max(inboundWaitSeconds, inboundHardWaitRaw);
|
|
4587
|
+
return {
|
|
4588
|
+
mode: parseMode(env.AGT_WEDGE_RESTART_MODE),
|
|
4589
|
+
inboundWaitSeconds,
|
|
4590
|
+
inboundHardWaitSeconds,
|
|
4591
|
+
paneStaleSeconds: parsePositiveInt(env.AGT_WEDGE_PANE_STALE_SECONDS, DEFAULTS.paneStaleSeconds, 30),
|
|
4592
|
+
transcriptStaleSeconds: parsePositiveInt(
|
|
4593
|
+
env.AGT_WEDGE_TRANSCRIPT_STALE_SECONDS,
|
|
4594
|
+
DEFAULTS.transcriptStaleSeconds,
|
|
4595
|
+
15
|
|
4596
|
+
),
|
|
4597
|
+
minCycles: parsePositiveInt(env.AGT_WEDGE_MIN_CYCLES, DEFAULTS.minCycles, 2)
|
|
4598
|
+
};
|
|
4599
|
+
}
|
|
4600
|
+
function isSessionProducing(signals, config2) {
|
|
4601
|
+
const subagentAge = signals.subagentActivityAgeSeconds;
|
|
4602
|
+
if (subagentAge !== null && subagentAge < config2.transcriptStaleSeconds) return true;
|
|
4603
|
+
const transcriptAge = signals.transcriptActivityAgeSeconds;
|
|
4604
|
+
if (transcriptAge !== null) return transcriptAge < config2.transcriptStaleSeconds;
|
|
4605
|
+
const paneAge = signals.paneActivityAgeSeconds;
|
|
4606
|
+
return paneAge !== null && paneAge < config2.paneStaleSeconds;
|
|
4607
|
+
}
|
|
4608
|
+
function wedgeExemptionReason(signals, config2) {
|
|
4609
|
+
if (signals.subagentActivityAgeSeconds === null) return null;
|
|
4610
|
+
if (isWedgeCandidateCycle(signals, config2)) return null;
|
|
4611
|
+
const withoutSubagent = { ...signals, subagentActivityAgeSeconds: null };
|
|
4612
|
+
return isWedgeCandidateCycle(withoutSubagent, config2) ? "background-task-in-flight" : null;
|
|
4613
|
+
}
|
|
4614
|
+
function isWedgeCandidateCycle(signals, config2) {
|
|
4615
|
+
const inboundAge = signals.pendingInboundOldestAgeSeconds;
|
|
4616
|
+
if (inboundAge === null) return false;
|
|
4617
|
+
if (inboundAge < config2.inboundWaitSeconds) return false;
|
|
4618
|
+
if (isSessionProducing(signals, config2)) {
|
|
4619
|
+
if (config2.inboundHardWaitSeconds <= 0) return false;
|
|
4620
|
+
return inboundAge >= config2.inboundHardWaitSeconds;
|
|
4621
|
+
}
|
|
4622
|
+
return true;
|
|
4623
|
+
}
|
|
4624
|
+
function decideWedgeRestart(input, config2) {
|
|
4625
|
+
if (!isWedgeCandidateCycle(input, config2)) return "none";
|
|
4626
|
+
return input.consecutiveWedgeCycles >= config2.minCycles ? "wedged" : "none";
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
// src/lib/wedge-poison-card.ts
|
|
4630
|
+
var DEFAULTS2 = {
|
|
4631
|
+
threshold: 3,
|
|
4632
|
+
cooldownSeconds: 1800
|
|
4633
|
+
// 30 min — matches the kanban stale-auto-fail window
|
|
4634
|
+
};
|
|
4635
|
+
function parsePositiveInt2(raw, fallback, floor) {
|
|
4636
|
+
const n = raw ? Number.parseInt(raw, 10) : NaN;
|
|
4637
|
+
return Number.isInteger(n) && n >= floor ? n : fallback;
|
|
4638
|
+
}
|
|
4639
|
+
function resolvePoisonCardConfig(env = process.env) {
|
|
4640
|
+
return {
|
|
4641
|
+
threshold: parsePositiveInt2(env.AGT_WEDGE_POISON_CARD_THRESHOLD, DEFAULTS2.threshold, 2),
|
|
4642
|
+
cooldownMs: parsePositiveInt2(
|
|
4643
|
+
env.AGT_WEDGE_POISON_CARD_COOLDOWN_SECONDS,
|
|
4644
|
+
DEFAULTS2.cooldownSeconds,
|
|
4645
|
+
300
|
|
4646
|
+
) * 1e3
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
function recordWedgeForCards(states, inProgressCardIds, nowMs, config2) {
|
|
4650
|
+
if (inProgressCardIds.length === 0) {
|
|
4651
|
+
return { next: new Map(states), newlyPoisoned: [] };
|
|
4652
|
+
}
|
|
4653
|
+
const next = /* @__PURE__ */ new Map();
|
|
4654
|
+
const newlyPoisoned = [];
|
|
4655
|
+
for (const id of inProgressCardIds) {
|
|
4656
|
+
if (next.has(id)) continue;
|
|
4657
|
+
const count = (states.get(id)?.count ?? 0) + 1;
|
|
4658
|
+
next.set(id, { count, lastWedgeAtMs: nowMs });
|
|
4659
|
+
if (count === config2.threshold) newlyPoisoned.push(id);
|
|
4660
|
+
}
|
|
4661
|
+
return { next, newlyPoisoned };
|
|
4662
|
+
}
|
|
4663
|
+
function pruneCardStates(states, liveInProgressCardIds, nowMs, config2) {
|
|
4664
|
+
const live = liveInProgressCardIds instanceof Set ? liveInProgressCardIds : new Set(liveInProgressCardIds);
|
|
4665
|
+
const next = /* @__PURE__ */ new Map();
|
|
4666
|
+
for (const [id, state7] of states) {
|
|
4667
|
+
if (!live.has(id)) continue;
|
|
4668
|
+
if (nowMs - state7.lastWedgeAtMs > config2.cooldownMs) continue;
|
|
4669
|
+
next.set(id, state7);
|
|
4670
|
+
}
|
|
4671
|
+
return next;
|
|
4672
|
+
}
|
|
4673
|
+
function isCardPoisoned(states, cardId, config2) {
|
|
4674
|
+
return (states.get(cardId)?.count ?? 0) >= config2.threshold;
|
|
4675
|
+
}
|
|
4676
|
+
function partitionActionableByPoison(actionable, states, config2) {
|
|
4677
|
+
const allowed = [];
|
|
4678
|
+
const suppressed = [];
|
|
4679
|
+
for (const item of actionable) {
|
|
4680
|
+
if (isCardPoisoned(states, item.id, config2)) suppressed.push(item);
|
|
4372
4681
|
else allowed.push(item);
|
|
4373
4682
|
}
|
|
4374
4683
|
return { allowed, suppressed };
|
|
@@ -5763,7 +6072,7 @@ var cachedMaintenanceWindow = null;
|
|
|
5763
6072
|
var lastVersionCheckAt = 0;
|
|
5764
6073
|
var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
5765
6074
|
var lastResponsivenessProbeAt = 0;
|
|
5766
|
-
var agtCliVersion = true ? "0.28.
|
|
6075
|
+
var agtCliVersion = true ? "0.28.29" : "dev";
|
|
5767
6076
|
function resolveBrewPath(execFileSync4) {
|
|
5768
6077
|
try {
|
|
5769
6078
|
const out = execFileSync4("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
@@ -6891,7 +7200,7 @@ async function pollCycle() {
|
|
|
6891
7200
|
}
|
|
6892
7201
|
try {
|
|
6893
7202
|
const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
|
|
6894
|
-
const { collectDiagnostics } = await import("../persistent-session-
|
|
7203
|
+
const { collectDiagnostics } = await import("../persistent-session-JHBXSNVW.js");
|
|
6895
7204
|
const diagCodeNames = [...agentState.persistentSessionAgents];
|
|
6896
7205
|
const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
|
|
6897
7206
|
let tailscaleHostname;
|
|
@@ -6985,7 +7294,7 @@ async function pollCycle() {
|
|
|
6985
7294
|
const {
|
|
6986
7295
|
collectResponsivenessProbes,
|
|
6987
7296
|
getResponsivenessIntervalMs
|
|
6988
|
-
} = await import("../responsiveness-probe-
|
|
7297
|
+
} = await import("../responsiveness-probe-SKVWT5CO.js");
|
|
6989
7298
|
const probeIntervalMs = getResponsivenessIntervalMs();
|
|
6990
7299
|
if (now - lastResponsivenessProbeAt > probeIntervalMs) {
|
|
6991
7300
|
const probeCodeNames = [...agentState.persistentSessionAgents];
|
|
@@ -7017,7 +7326,7 @@ async function pollCycle() {
|
|
|
7017
7326
|
collectResponsivenessProbes,
|
|
7018
7327
|
livePendingInboundOldestAgeSeconds,
|
|
7019
7328
|
parkPendingInbound
|
|
7020
|
-
} = await import("../responsiveness-probe-
|
|
7329
|
+
} = await import("../responsiveness-probe-SKVWT5CO.js");
|
|
7021
7330
|
const { getProjectDir: wedgeProjectDir } = await import("../claude-scheduler-FATCLHDM.js");
|
|
7022
7331
|
const wedgeNow = /* @__PURE__ */ new Date();
|
|
7023
7332
|
const liveAgents = agentState.persistentSessionAgents;
|
|
@@ -10774,7 +11083,6 @@ var lastHarvestAt = /* @__PURE__ */ new Map();
|
|
|
10774
11083
|
var HARVEST_INTERVAL_MS = 3 * 60 * 1e3;
|
|
10775
11084
|
var kanbanBoardCache = /* @__PURE__ */ new Map();
|
|
10776
11085
|
var notifyBoardCache = /* @__PURE__ */ new Map();
|
|
10777
|
-
var NOTIFY_RESULT_MAX_CHARS = 3500;
|
|
10778
11086
|
async function harvestCronResults(codeName, tasks, gatewayPort) {
|
|
10779
11087
|
const token = readGatewayToken(codeName);
|
|
10780
11088
|
const gwArgs = ["--url", `ws://127.0.0.1:${gatewayPort}`, ...token ? ["--token", token] : []];
|
|
@@ -10873,339 +11181,107 @@ async function harvestCronResults(codeName, tasks, gatewayPort) {
|
|
|
10873
11181
|
const summary = latest.summary ?? "";
|
|
10874
11182
|
const kanbanUpdates = parseKanbanUpdates(summary);
|
|
10875
11183
|
if (kanbanUpdates.length > 0) {
|
|
10876
|
-
try {
|
|
10877
|
-
const agentId = agentState.codeNameToAgentId.get(codeName);
|
|
10878
|
-
if (agentId) {
|
|
10879
|
-
await api.post("/host/kanban", {
|
|
10880
|
-
agent_id: agentId,
|
|
10881
|
-
update: kanbanUpdates
|
|
10882
|
-
});
|
|
10883
|
-
log(`Updated ${kanbanUpdates.length} kanban items for '${codeName}'`);
|
|
10884
|
-
}
|
|
10885
|
-
} catch (err) {
|
|
10886
|
-
log(`Failed to update kanban for '${codeName}': ${err.message}`);
|
|
10887
|
-
}
|
|
10888
|
-
}
|
|
10889
|
-
}
|
|
10890
|
-
}
|
|
10891
|
-
}
|
|
10892
|
-
var LATE_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
10893
|
-
async function monitorCronHealth(agentStates) {
|
|
10894
|
-
const alerts = [];
|
|
10895
|
-
const now = Date.now();
|
|
10896
|
-
for (const agent of agentStates) {
|
|
10897
|
-
if (!agent.gatewayRunning || !agent.gatewayPort) continue;
|
|
10898
|
-
const token = readGatewayToken(agent.codeName);
|
|
10899
|
-
const gwArgs = ["--url", `ws://127.0.0.1:${agent.gatewayPort}`, ...token ? ["--token", token] : []];
|
|
10900
|
-
let jobs = [];
|
|
10901
|
-
try {
|
|
10902
|
-
const cliBin = resolveAgentFramework(agent.codeName).cliBinary ?? "openclaw";
|
|
10903
|
-
const { stdout } = await execFilePromise(cliBin, ["--profile", agent.codeName, "cron", "list", "--json", ...gwArgs]);
|
|
10904
|
-
const parsed = JSON.parse(stdout);
|
|
10905
|
-
jobs = parsed.jobs ?? [];
|
|
10906
|
-
} catch {
|
|
10907
|
-
continue;
|
|
10908
|
-
}
|
|
10909
|
-
for (const job of jobs) {
|
|
10910
|
-
if (!job.enabled || !job.name.startsWith("aug:")) continue;
|
|
10911
|
-
const alertKey = `${agent.codeName}:${job.id}`;
|
|
10912
|
-
const displayInfo = taskDisplayInfo.get(`${agent.codeName}:${job.name}`);
|
|
10913
|
-
const taskName = displayInfo?.taskName ?? job.name;
|
|
10914
|
-
const schedule = displayInfo?.schedule ?? "";
|
|
10915
|
-
const agentDisplayName = displayInfo?.agentDisplayName ?? agent.codeName;
|
|
10916
|
-
if (job.state?.nextRunAtMs && job.state.nextRunAtMs + LATE_THRESHOLD_MS < now) {
|
|
10917
|
-
if (!alertedJobs.has(`late:${alertKey}`)) {
|
|
10918
|
-
const minsLate = Math.round((now - job.state.nextRunAtMs) / 6e4);
|
|
10919
|
-
alerts.push({
|
|
10920
|
-
type: "late_standup",
|
|
10921
|
-
agentCodeName: agent.codeName,
|
|
10922
|
-
agentDisplayName,
|
|
10923
|
-
jobName: job.name,
|
|
10924
|
-
taskName,
|
|
10925
|
-
schedule,
|
|
10926
|
-
jobId: job.id,
|
|
10927
|
-
detail: `Job is ${minsLate}m late (expected at ${new Date(job.state.nextRunAtMs).toISOString()})`
|
|
10928
|
-
});
|
|
10929
|
-
alertedJobs.add(`late:${alertKey}`);
|
|
10930
|
-
}
|
|
10931
|
-
} else {
|
|
10932
|
-
alertedJobs.delete(`late:${alertKey}`);
|
|
10933
|
-
}
|
|
10934
|
-
if (job.state?.lastRunStatus === "error" || job.state?.consecutiveErrors && job.state.consecutiveErrors > 0) {
|
|
10935
|
-
if (!alertedJobs.has(`fail:${alertKey}`)) {
|
|
10936
|
-
alerts.push({
|
|
10937
|
-
type: "cron_failure",
|
|
10938
|
-
agentCodeName: agent.codeName,
|
|
10939
|
-
agentDisplayName,
|
|
10940
|
-
jobName: job.name,
|
|
10941
|
-
taskName,
|
|
10942
|
-
schedule,
|
|
10943
|
-
jobId: job.id,
|
|
10944
|
-
detail: `Last run failed (${job.state.consecutiveErrors ?? 1} consecutive error(s))`
|
|
10945
|
-
});
|
|
10946
|
-
alertedJobs.add(`fail:${alertKey}`);
|
|
10947
|
-
}
|
|
10948
|
-
} else {
|
|
10949
|
-
alertedJobs.delete(`fail:${alertKey}`);
|
|
10950
|
-
}
|
|
10951
|
-
}
|
|
10952
|
-
}
|
|
10953
|
-
if (alerts.length === 0) return;
|
|
10954
|
-
for (const alert of alerts) {
|
|
10955
|
-
log(`ALERT [${alert.type}] ${alert.agentCodeName}/${alert.jobName}: ${alert.detail}`);
|
|
10956
|
-
}
|
|
10957
|
-
if (getAlertSlackWebhook()) {
|
|
10958
|
-
await sendSlackAlert(alerts);
|
|
10959
|
-
}
|
|
10960
|
-
try {
|
|
10961
|
-
await api.post("/host/cron-alerts", { alerts });
|
|
10962
|
-
} catch {
|
|
10963
|
-
}
|
|
10964
|
-
}
|
|
10965
|
-
async function sendSlackAlert(alerts) {
|
|
10966
|
-
const blocks = alerts.map((a) => {
|
|
10967
|
-
const emoji = a.type === "late_standup" ? ":warning:" : ":x:";
|
|
10968
|
-
const label = a.type === "late_standup" ? "Late" : "Failed";
|
|
10969
|
-
const scheduleInfo = a.schedule ? ` (${a.schedule})` : "";
|
|
10970
|
-
return `${emoji} *${label}* \u2014 *${a.agentDisplayName}* / ${a.taskName}${scheduleInfo}
|
|
10971
|
-
${a.detail}`;
|
|
10972
|
-
});
|
|
10973
|
-
await sendSlackWebhookMessage(`:rotating_light: *Cron Health Alert*
|
|
10974
|
-
|
|
10975
|
-
${blocks.join("\n\n")}`);
|
|
10976
|
-
}
|
|
10977
|
-
async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, body, taskId) {
|
|
10978
|
-
const withLink = (b, medium) => withScheduleLinkFooter({ body: b, medium, codeName: agentCodeName, agentId, taskId, log });
|
|
10979
|
-
if (typeof rawTarget === "string") {
|
|
10980
|
-
if (rawTarget.startsWith("channel:")) {
|
|
10981
|
-
const result = await sendTaskNotification(agentCodeName, "slack", rawTarget, withLink(body, "slack"));
|
|
10982
|
-
await reportDeliveryStatus(agentId, taskId, {
|
|
10983
|
-
status: result.ok ? "ok" : "failed",
|
|
10984
|
-
medium: "slack",
|
|
10985
|
-
error_code: result.ok ? null : result.error_code ?? "SLACK_SEND_FAILED"
|
|
10986
|
-
});
|
|
10987
|
-
return;
|
|
10988
|
-
}
|
|
10989
|
-
if (rawTarget.startsWith("chat:")) {
|
|
10990
|
-
const result = await sendTaskNotification(agentCodeName, "telegram", rawTarget, withLink(body, "telegram"));
|
|
10991
|
-
await reportDeliveryStatus(agentId, taskId, {
|
|
10992
|
-
status: result.ok ? "ok" : "failed",
|
|
10993
|
-
medium: "telegram",
|
|
10994
|
-
error_code: result.ok ? null : result.error_code ?? "TELEGRAM_SEND_FAILED"
|
|
10995
|
-
});
|
|
10996
|
-
return;
|
|
10997
|
-
}
|
|
10998
|
-
log(`[delivery] Unrecognised legacy delivery_to string for '${agentCodeName}': ${rawTarget.slice(0, 60)}`);
|
|
10999
|
-
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "LEGACY_DELIVERY_TO_UNRECOGNISED" });
|
|
11000
|
-
return;
|
|
11001
|
-
}
|
|
11002
|
-
const parsed = parseDeliveryTarget(rawTarget);
|
|
11003
|
-
if (isParseError(parsed)) {
|
|
11004
|
-
log(`[delivery] Malformed delivery_to for '${agentCodeName}': ${parsed.code} \u2014 ${parsed.detail}`);
|
|
11005
|
-
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: parsed.code });
|
|
11006
|
-
return;
|
|
11007
|
-
}
|
|
11008
|
-
if (parsed.kind === "channel") {
|
|
11009
|
-
if (parsed.provider === "slack") {
|
|
11010
|
-
const channelId = parsed.channel_id ?? "";
|
|
11011
|
-
const threadTs = parsed.thread_ts;
|
|
11012
|
-
let sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"), threadTs);
|
|
11013
|
-
let deliveredInThread = Boolean(threadTs);
|
|
11014
|
-
if (!sent.ok && threadTs && (sent.error === "thread_not_found" || sent.error === "message_not_found")) {
|
|
11015
|
-
log(`[delivery] Originating thread ${threadTs} gone in ${channelId} for '${agentCodeName}' \u2014 falling back to top-level post`);
|
|
11016
|
-
sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"));
|
|
11017
|
-
deliveredInThread = false;
|
|
11018
|
-
}
|
|
11019
|
-
await reportDeliveryStatus(agentId, taskId, {
|
|
11020
|
-
status: sent.ok ? "ok" : "failed",
|
|
11021
|
-
medium: "slack",
|
|
11022
|
-
// Preserve Slack's concrete failure code (channel_not_found,
|
|
11023
|
-
// not_in_channel, auth errors, …) in the observability row rather
|
|
11024
|
-
// than collapsing everything to the generic constant.
|
|
11025
|
-
error_code: sent.ok ? null : sent.error ? `SLACK_SEND_FAILED:${sent.error}` : "SLACK_SEND_FAILED"
|
|
11026
|
-
});
|
|
11027
|
-
if (sent.ok) {
|
|
11028
|
-
if (!deliveredInThread) {
|
|
11029
|
-
await maybePostSlackThreadHint(agentCodeName, channelId, sent.ts);
|
|
11030
|
-
}
|
|
11031
|
-
if (sent.ts && taskId) {
|
|
11032
|
-
await maybePostScheduledTaskRatingPrompt(agentId, taskId, channelId, sent.ts);
|
|
11033
|
-
}
|
|
11034
|
-
}
|
|
11035
|
-
return;
|
|
11036
|
-
}
|
|
11037
|
-
const chatId = parsed.chat_id ?? "";
|
|
11038
|
-
const toStr = `chat:${chatId}`;
|
|
11039
|
-
const result = await sendTaskNotification(agentCodeName, "telegram", toStr, withLink(body, "telegram"));
|
|
11040
|
-
await reportDeliveryStatus(agentId, taskId, {
|
|
11041
|
-
status: result.ok ? "ok" : "failed",
|
|
11042
|
-
medium: "telegram",
|
|
11043
|
-
error_code: result.ok ? null : result.error_code ?? "TELEGRAM_SEND_FAILED"
|
|
11044
|
-
});
|
|
11045
|
-
if (result.ok) {
|
|
11046
|
-
const botToken = agentChannelTokens.get(agentCodeName)?.telegram;
|
|
11047
|
-
if (botToken) await maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId);
|
|
11048
|
-
}
|
|
11049
|
-
return;
|
|
11050
|
-
}
|
|
11051
|
-
const agentRow = agentInfoForDelivery.get(agentCodeName);
|
|
11052
|
-
if (!agentRow) {
|
|
11053
|
-
log(`[delivery] No agent metadata cached for '${agentCodeName}' \u2014 dropping DM`);
|
|
11054
|
-
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "AGENT_METADATA_UNAVAILABLE" });
|
|
11055
|
-
return;
|
|
11056
|
-
}
|
|
11057
|
-
const resolved = resolveDmTarget(parsed, agentRow.resolverAgent, agentRow.peopleByPersonId);
|
|
11058
|
-
if (isResolveError(resolved)) {
|
|
11059
|
-
log(`[delivery] Cannot resolve DM target for '${agentCodeName}': ${resolved.code} \u2014 ${resolved.detail}`);
|
|
11060
|
-
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: resolved.code });
|
|
11061
|
-
return;
|
|
11062
|
-
}
|
|
11063
|
-
const attributionBody = appendDmFooter(
|
|
11064
|
-
body,
|
|
11065
|
-
agentRow.ownerTeamName,
|
|
11066
|
-
agentRow.agentDisplayName
|
|
11067
|
-
);
|
|
11068
|
-
const footeredBody = resolved.kind === "dm" ? withLink(attributionBody, resolved.medium === "slack" ? "slack" : "telegram") : attributionBody;
|
|
11069
|
-
if (resolved.kind === "dm" && resolved.medium === "slack") {
|
|
11070
|
-
const tokens = agentChannelTokens.get(agentCodeName);
|
|
11071
|
-
const botToken = tokens?.slack;
|
|
11072
|
-
if (!botToken) {
|
|
11073
|
-
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "SLACK_MISSING_SCOPE", medium: "slack" });
|
|
11074
|
-
log(`[delivery] No Slack bot token for '${agentCodeName}' \u2014 DM dropped`);
|
|
11075
|
-
return;
|
|
11076
|
-
}
|
|
11077
|
-
try {
|
|
11078
|
-
const controller = new AbortController();
|
|
11079
|
-
const timeoutHandle = setTimeout(() => controller.abort(), 5e3);
|
|
11080
|
-
let openJson;
|
|
11081
|
-
try {
|
|
11082
|
-
const openResp = await fetch("https://slack.com/api/conversations.open", {
|
|
11083
|
-
method: "POST",
|
|
11084
|
-
headers: {
|
|
11085
|
-
Authorization: `Bearer ${botToken}`,
|
|
11086
|
-
"Content-Type": "application/json; charset=utf-8"
|
|
11087
|
-
},
|
|
11088
|
-
body: JSON.stringify({ users: resolved.slack_user_id }),
|
|
11089
|
-
signal: controller.signal
|
|
11090
|
-
});
|
|
11091
|
-
openJson = await openResp.json();
|
|
11092
|
-
} finally {
|
|
11093
|
-
clearTimeout(timeoutHandle);
|
|
11094
|
-
}
|
|
11095
|
-
if (!openJson.ok || !openJson.channel?.id) {
|
|
11096
|
-
const errCode = openJson.error === "missing_scope" ? "SLACK_MISSING_SCOPE" : `SLACK_OPEN_FAILED:${openJson.error ?? "unknown"}`;
|
|
11097
|
-
log(`[delivery] conversations.open failed for '${agentCodeName}': ${openJson.error}`);
|
|
11098
|
-
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: errCode, medium: "slack" });
|
|
11099
|
-
return;
|
|
11184
|
+
try {
|
|
11185
|
+
const agentId = agentState.codeNameToAgentId.get(codeName);
|
|
11186
|
+
if (agentId) {
|
|
11187
|
+
await api.post("/host/kanban", {
|
|
11188
|
+
agent_id: agentId,
|
|
11189
|
+
update: kanbanUpdates
|
|
11190
|
+
});
|
|
11191
|
+
log(`Updated ${kanbanUpdates.length} kanban items for '${codeName}'`);
|
|
11192
|
+
}
|
|
11193
|
+
} catch (err) {
|
|
11194
|
+
log(`Failed to update kanban for '${codeName}': ${err.message}`);
|
|
11195
|
+
}
|
|
11100
11196
|
}
|
|
11101
|
-
const sent = await postSlackChannelMessage(agentCodeName, openJson.channel.id, footeredBody);
|
|
11102
|
-
await reportDeliveryStatus(agentId, taskId, {
|
|
11103
|
-
status: sent.ok ? "ok" : "failed",
|
|
11104
|
-
medium: "slack",
|
|
11105
|
-
error_code: sent.ok ? null : "SLACK_SEND_FAILED"
|
|
11106
|
-
});
|
|
11107
|
-
if (sent.ok) await maybePostSlackThreadHint(agentCodeName, openJson.channel.id, sent.ts);
|
|
11108
|
-
} catch (err) {
|
|
11109
|
-
const isAbort = err.name === "AbortError";
|
|
11110
|
-
const errCode = isAbort ? "SLACK_OPEN_TIMEOUT" : "SLACK_EXCEPTION";
|
|
11111
|
-
log(`[delivery] Slack DM ${isAbort ? "timeout" : "failure"} for '${agentCodeName}': ${err.message}`);
|
|
11112
|
-
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: errCode, medium: "slack" });
|
|
11113
11197
|
}
|
|
11114
|
-
return;
|
|
11115
11198
|
}
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11199
|
+
}
|
|
11200
|
+
var LATE_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
11201
|
+
async function monitorCronHealth(agentStates) {
|
|
11202
|
+
const alerts = [];
|
|
11203
|
+
const now = Date.now();
|
|
11204
|
+
for (const agent of agentStates) {
|
|
11205
|
+
if (!agent.gatewayRunning || !agent.gatewayPort) continue;
|
|
11206
|
+
const token = readGatewayToken(agent.codeName);
|
|
11207
|
+
const gwArgs = ["--url", `ws://127.0.0.1:${agent.gatewayPort}`, ...token ? ["--token", token] : []];
|
|
11208
|
+
let jobs = [];
|
|
11124
11209
|
try {
|
|
11125
|
-
const
|
|
11126
|
-
|
|
11127
|
-
|
|
11128
|
-
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11210
|
+
const cliBin = resolveAgentFramework(agent.codeName).cliBinary ?? "openclaw";
|
|
11211
|
+
const { stdout } = await execFilePromise(cliBin, ["--profile", agent.codeName, "cron", "list", "--json", ...gwArgs]);
|
|
11212
|
+
const parsed = JSON.parse(stdout);
|
|
11213
|
+
jobs = parsed.jobs ?? [];
|
|
11214
|
+
} catch {
|
|
11215
|
+
continue;
|
|
11216
|
+
}
|
|
11217
|
+
for (const job of jobs) {
|
|
11218
|
+
if (!job.enabled || !job.name.startsWith("aug:")) continue;
|
|
11219
|
+
const alertKey = `${agent.codeName}:${job.id}`;
|
|
11220
|
+
const displayInfo = taskDisplayInfo.get(`${agent.codeName}:${job.name}`);
|
|
11221
|
+
const taskName = displayInfo?.taskName ?? job.name;
|
|
11222
|
+
const schedule = displayInfo?.schedule ?? "";
|
|
11223
|
+
const agentDisplayName = displayInfo?.agentDisplayName ?? agent.codeName;
|
|
11224
|
+
if (job.state?.nextRunAtMs && job.state.nextRunAtMs + LATE_THRESHOLD_MS < now) {
|
|
11225
|
+
if (!alertedJobs.has(`late:${alertKey}`)) {
|
|
11226
|
+
const minsLate = Math.round((now - job.state.nextRunAtMs) / 6e4);
|
|
11227
|
+
alerts.push({
|
|
11228
|
+
type: "late_standup",
|
|
11229
|
+
agentCodeName: agent.codeName,
|
|
11230
|
+
agentDisplayName,
|
|
11231
|
+
jobName: job.name,
|
|
11232
|
+
taskName,
|
|
11233
|
+
schedule,
|
|
11234
|
+
jobId: job.id,
|
|
11235
|
+
detail: `Job is ${minsLate}m late (expected at ${new Date(job.state.nextRunAtMs).toISOString()})`
|
|
11236
|
+
});
|
|
11237
|
+
alertedJobs.add(`late:${alertKey}`);
|
|
11238
|
+
}
|
|
11239
|
+
} else {
|
|
11240
|
+
alertedJobs.delete(`late:${alertKey}`);
|
|
11241
|
+
}
|
|
11242
|
+
if (job.state?.lastRunStatus === "error" || job.state?.consecutiveErrors && job.state.consecutiveErrors > 0) {
|
|
11243
|
+
if (!alertedJobs.has(`fail:${alertKey}`)) {
|
|
11244
|
+
alerts.push({
|
|
11245
|
+
type: "cron_failure",
|
|
11246
|
+
agentCodeName: agent.codeName,
|
|
11247
|
+
agentDisplayName,
|
|
11248
|
+
jobName: job.name,
|
|
11249
|
+
taskName,
|
|
11250
|
+
schedule,
|
|
11251
|
+
jobId: job.id,
|
|
11252
|
+
detail: `Last run failed (${job.state.consecutiveErrors ?? 1} consecutive error(s))`
|
|
11253
|
+
});
|
|
11254
|
+
alertedJobs.add(`fail:${alertKey}`);
|
|
11255
|
+
}
|
|
11256
|
+
} else {
|
|
11257
|
+
alertedJobs.delete(`fail:${alertKey}`);
|
|
11133
11258
|
}
|
|
11134
|
-
await reportDeliveryStatus(agentId, taskId, { status: "ok", medium: "telegram" });
|
|
11135
|
-
await maybeSendTelegramFollowUpHint(agentCodeName, botToken, resolved.telegram_chat_id);
|
|
11136
|
-
} catch (err) {
|
|
11137
|
-
log(`[delivery] Telegram DM exception for '${agentCodeName}': ${err.message}`);
|
|
11138
|
-
await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "TELEGRAM_EXCEPTION", medium: "telegram" });
|
|
11139
11259
|
}
|
|
11140
11260
|
}
|
|
11141
|
-
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
const agentRow = refreshData["agent"] ?? {};
|
|
11145
|
-
const displayName = agentRow["display_name"] ?? codeName;
|
|
11146
|
-
const ownerTeamName = agentRow["owner_team_name"] ?? null;
|
|
11147
|
-
const framework = agentRow["framework"] ?? "openclaw";
|
|
11148
|
-
const reportsToPersonId = agentRow["reports_to"] ?? null;
|
|
11149
|
-
const reportsToType = agentRow["reports_to_type"] ?? null;
|
|
11150
|
-
const channelConfigs = refreshData["channel_configs"] ?? {};
|
|
11151
|
-
const dmCapable = [];
|
|
11152
|
-
const slackBotToken = channelConfigs["slack"]?.config?.["bot_token"];
|
|
11153
|
-
if (typeof slackBotToken === "string" && slackBotToken.length > 0) {
|
|
11154
|
-
dmCapable.push("slack");
|
|
11155
|
-
}
|
|
11156
|
-
const telegramBotToken = channelConfigs["telegram"]?.config?.["bot_token"];
|
|
11157
|
-
if (typeof telegramBotToken === "string" && telegramBotToken.length > 0) {
|
|
11158
|
-
dmCapable.push("telegram");
|
|
11159
|
-
}
|
|
11160
|
-
const resolverAgent = {
|
|
11161
|
-
agent_id: agentRow["agent_id"] ?? "",
|
|
11162
|
-
framework,
|
|
11163
|
-
dm_capable_mediums: dmCapable,
|
|
11164
|
-
reports_to_person_id: reportsToType === "person" ? reportsToPersonId : null,
|
|
11165
|
-
reports_to_type: reportsToType
|
|
11166
|
-
};
|
|
11167
|
-
const peopleByPersonId = /* @__PURE__ */ new Map();
|
|
11168
|
-
if (reportsToPersonId && reportsToType === "person") {
|
|
11169
|
-
peopleByPersonId.set(reportsToPersonId, {
|
|
11170
|
-
person_id: reportsToPersonId,
|
|
11171
|
-
display_name: agentRow["reports_to_name"] ?? "Reports-To",
|
|
11172
|
-
slack_user_id: agentRow["reports_to_slack_user_id"] ?? null,
|
|
11173
|
-
telegram_chat_id: agentRow["reports_to_telegram_chat_id"] ?? null
|
|
11174
|
-
});
|
|
11261
|
+
if (alerts.length === 0) return;
|
|
11262
|
+
for (const alert of alerts) {
|
|
11263
|
+
log(`ALERT [${alert.type}] ${alert.agentCodeName}/${alert.jobName}: ${alert.detail}`);
|
|
11175
11264
|
}
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
const personId = p["person_id"];
|
|
11179
|
-
if (!personId) continue;
|
|
11180
|
-
const contactPrefs = p["contact_preferences"] ?? {};
|
|
11181
|
-
peopleByPersonId.set(personId, {
|
|
11182
|
-
person_id: personId,
|
|
11183
|
-
display_name: p["display_name"] ?? "person",
|
|
11184
|
-
slack_user_id: contactPrefs["slack_user_id"] ?? null,
|
|
11185
|
-
telegram_chat_id: contactPrefs["telegram_chat_id"] ?? null
|
|
11186
|
-
});
|
|
11265
|
+
if (getAlertSlackWebhook()) {
|
|
11266
|
+
await sendSlackAlert(alerts);
|
|
11187
11267
|
}
|
|
11188
|
-
agentInfoForDelivery.set(codeName, {
|
|
11189
|
-
agentDisplayName: displayName,
|
|
11190
|
-
ownerTeamName,
|
|
11191
|
-
resolverAgent,
|
|
11192
|
-
peopleByPersonId
|
|
11193
|
-
});
|
|
11194
|
-
}
|
|
11195
|
-
async function reportDeliveryStatus(agentId, taskId, payload) {
|
|
11196
|
-
if (!taskId) return;
|
|
11197
11268
|
try {
|
|
11198
|
-
await api.post("/host/
|
|
11199
|
-
|
|
11200
|
-
task_id: taskId,
|
|
11201
|
-
status: payload.status,
|
|
11202
|
-
medium: payload.medium ?? null,
|
|
11203
|
-
error_code: payload.error_code ?? null
|
|
11204
|
-
});
|
|
11205
|
-
} catch (err) {
|
|
11206
|
-
log(`[delivery] Failed to report delivery status for ${agentId}/${taskId}: ${err.message}`);
|
|
11269
|
+
await api.post("/host/cron-alerts", { alerts });
|
|
11270
|
+
} catch {
|
|
11207
11271
|
}
|
|
11208
11272
|
}
|
|
11273
|
+
async function sendSlackAlert(alerts) {
|
|
11274
|
+
const blocks = alerts.map((a) => {
|
|
11275
|
+
const emoji = a.type === "late_standup" ? ":warning:" : ":x:";
|
|
11276
|
+
const label = a.type === "late_standup" ? "Late" : "Failed";
|
|
11277
|
+
const scheduleInfo = a.schedule ? ` (${a.schedule})` : "";
|
|
11278
|
+
return `${emoji} *${label}* \u2014 *${a.agentDisplayName}* / ${a.taskName}${scheduleInfo}
|
|
11279
|
+
${a.detail}`;
|
|
11280
|
+
});
|
|
11281
|
+
await sendSlackWebhookMessage(`:rotating_light: *Cron Health Alert*
|
|
11282
|
+
|
|
11283
|
+
${blocks.join("\n\n")}`);
|
|
11284
|
+
}
|
|
11209
11285
|
var spawnedPairIds = /* @__PURE__ */ new Set();
|
|
11210
11286
|
async function processClaudePairSessions(agents) {
|
|
11211
11287
|
if (agents.length === 0 && spawnedPairIds.size === 0) return;
|
|
@@ -11222,7 +11298,7 @@ async function processClaudePairSessions(agents) {
|
|
|
11222
11298
|
killPairSession,
|
|
11223
11299
|
pairTmuxSession,
|
|
11224
11300
|
finalizeClaudePairOnboarding
|
|
11225
|
-
} = await import("../claude-pair-runtime-
|
|
11301
|
+
} = await import("../claude-pair-runtime-RLIUZRLZ.js");
|
|
11226
11302
|
for (const pairId of pendingResp.cancelled_pair_ids ?? []) {
|
|
11227
11303
|
log(`[claude-pair] sweeping orphan tmux session for pair ${pairId.slice(0, 8)}`);
|
|
11228
11304
|
const killed = await killPairSession(pairTmuxSession(pairId));
|
|
@@ -11329,76 +11405,6 @@ async function processClaudePairSessions(agents) {
|
|
|
11329
11405
|
}
|
|
11330
11406
|
}
|
|
11331
11407
|
}
|
|
11332
|
-
async function maybePostScheduledTaskRatingPrompt(agentId, taskId, channelId, messageTs) {
|
|
11333
|
-
try {
|
|
11334
|
-
await api.post("/host/scheduled-task/rating-prompt", {
|
|
11335
|
-
agent_id: agentId,
|
|
11336
|
-
task_id: taskId,
|
|
11337
|
-
channel: channelId,
|
|
11338
|
-
message_ts: messageTs
|
|
11339
|
-
});
|
|
11340
|
-
} catch (err) {
|
|
11341
|
-
log(`[rating-prompt] Failed to post rating prompt for ${agentId}/${taskId}: ${err.message}`);
|
|
11342
|
-
}
|
|
11343
|
-
}
|
|
11344
|
-
async function buildDoneCardNotification(agentId, codeName, displayName, item) {
|
|
11345
|
-
const isFailed = item.status === "failed";
|
|
11346
|
-
let resultBody = item.result ?? "";
|
|
11347
|
-
try {
|
|
11348
|
-
const full = await api.post("/host/kanban-item", {
|
|
11349
|
-
agent_id: agentId,
|
|
11350
|
-
item_id: item.id
|
|
11351
|
-
});
|
|
11352
|
-
if (full.item?.result) resultBody = full.item.result;
|
|
11353
|
-
} catch (err) {
|
|
11354
|
-
log(`[notify] full-result fetch failed for card ${item.id} on '${codeName}': ${err.message} \u2014 using truncated board result`);
|
|
11355
|
-
}
|
|
11356
|
-
const resultLine = resultBody ? `
|
|
11357
|
-
${isFailed ? "Reason" : "Result"}: ${resultBody.slice(0, NOTIFY_RESULT_MAX_CHARS)}` : "";
|
|
11358
|
-
const emoji = isFailed ? "\u274C" : "\u2705";
|
|
11359
|
-
return `${emoji} ${isFailed ? "Task Failed" : "Task Complete"} \u2014 ${displayName}
|
|
11360
|
-
${item.title}${resultLine}`;
|
|
11361
|
-
}
|
|
11362
|
-
async function sendTaskNotification(agentCodeName, channel, to, text) {
|
|
11363
|
-
const tokens = agentChannelTokens.get(agentCodeName);
|
|
11364
|
-
if (channel === "slack") {
|
|
11365
|
-
const botToken = tokens?.slack;
|
|
11366
|
-
const channelId = to.replace(/^channel:/, "");
|
|
11367
|
-
if (!botToken) {
|
|
11368
|
-
log(`No Slack bot token for '${agentCodeName}' \u2014 targeted notification dropped`);
|
|
11369
|
-
return { ok: false, error_code: "SLACK_NO_TOKEN" };
|
|
11370
|
-
}
|
|
11371
|
-
const sent = await sendSlackChannelMessage(agentCodeName, channelId, text);
|
|
11372
|
-
return sent ? { ok: true } : { ok: false, error_code: "SLACK_SEND_FAILED" };
|
|
11373
|
-
}
|
|
11374
|
-
if (channel === "telegram") {
|
|
11375
|
-
const botToken = tokens?.telegram;
|
|
11376
|
-
const chatId = to.replace(/^chat:/, "");
|
|
11377
|
-
if (!botToken) {
|
|
11378
|
-
log(`No Telegram bot token for '${agentCodeName}' \u2014 notification dropped`);
|
|
11379
|
-
return { ok: false, error_code: "TELEGRAM_NO_TOKEN" };
|
|
11380
|
-
}
|
|
11381
|
-
const allowedChats = tokens?.telegramAllowedChats;
|
|
11382
|
-
if (allowedChats && allowedChats.length > 0 && !allowedChats.includes(chatId)) {
|
|
11383
|
-
log(`Telegram chat ${chatId} not in allowed_chat_ids for '${agentCodeName}'`);
|
|
11384
|
-
return { ok: false, error_code: "TELEGRAM_CHAT_NOT_ALLOWED" };
|
|
11385
|
-
}
|
|
11386
|
-
try {
|
|
11387
|
-
const result = await telegramApiCall(botToken, "sendMessage", { chat_id: chatId, text });
|
|
11388
|
-
if (!result.ok) {
|
|
11389
|
-
log(`Telegram sendMessage failed for '${agentCodeName}': ${result.description}`);
|
|
11390
|
-
return { ok: false, error_code: `TELEGRAM_SEND_FAILED:${result.description ?? "unknown"}` };
|
|
11391
|
-
}
|
|
11392
|
-
log(`Telegram notification sent for '${agentCodeName}' to chat ${chatId}`);
|
|
11393
|
-
return { ok: true };
|
|
11394
|
-
} catch (err) {
|
|
11395
|
-
log(`Telegram API error for '${agentCodeName}': ${err.message}`);
|
|
11396
|
-
return { ok: false, error_code: "TELEGRAM_EXCEPTION" };
|
|
11397
|
-
}
|
|
11398
|
-
}
|
|
11399
|
-
log(`Unknown notify_channel '${channel}' for '${agentCodeName}'`);
|
|
11400
|
-
return { ok: false, error_code: `UNKNOWN_CHANNEL:${channel}` };
|
|
11401
|
-
}
|
|
11402
11408
|
function generateArtifacts(agent, refreshData, adapter) {
|
|
11403
11409
|
if (!refreshData.charter || !refreshData.tools) {
|
|
11404
11410
|
throw new Error("No charter/tools available");
|
|
@@ -12178,13 +12184,11 @@ export {
|
|
|
12178
12184
|
SCHEDULED_CARD_DELIVERY_CONTRACT,
|
|
12179
12185
|
__resetScheduledDeliveryDedupeForTest,
|
|
12180
12186
|
applyRestartAcks,
|
|
12181
|
-
buildDoneCardNotification,
|
|
12182
12187
|
buildSchedulerTaskInput,
|
|
12183
12188
|
claudeCodeUpgradeMarkerPath,
|
|
12184
12189
|
claudeCodeUpgradeThrottled,
|
|
12185
12190
|
claudeManagedSettingsPath,
|
|
12186
12191
|
deliverScheduledCardResult,
|
|
12187
|
-
deliverScheduledTaskOutput,
|
|
12188
12192
|
deriveMcpServerKey,
|
|
12189
12193
|
deriveScheduledTaskNotify,
|
|
12190
12194
|
ensureClaudeManagedSettings,
|