@integrity-labs/agt-cli 0.28.28 → 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.
@@ -22,7 +22,7 @@ import {
22
22
  provisionStopHook,
23
23
  requireHost,
24
24
  safeWriteJsonAtomic
25
- } from "../chunk-GW2FVIMZ.js";
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-IHPN6AX7.js";
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-SN2G4B2Z.js";
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 envSuffixFor2(codeName) {
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 ? `__${envSuffixFor2(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/wedge-detection.ts
4240
- var DEFAULTS = {
4241
- inboundWaitSeconds: 120,
4242
- // ENG-6264: DISABLED by default (0). A session that's actively producing
4243
- // tokens is never force-respawned — a working agent must not be killed just
4244
- // because a message has been queued behind its turn, no matter how long.
4245
- // ENG-6238 made this an absolute backstop (1200s) to still catch a
4246
- // producing-but-never-draining runaway loop, but that re-introduced the exact
4247
- // failure we set out to kill: cutting off real work on a long turn. Runaway
4248
- // token burn is owned by the cost guardrail (ENG-5556); a producing-but-silent
4249
- // loop still trips the synthetic-probe alarm. So the backstop is now opt-in:
4250
- // set AGT_WEDGE_INBOUND_HARD_WAIT_SECONDS to a positive value to re-enable it
4251
- // (floored at inboundWaitSeconds). 0 = the frozen/hung wedge (transcript
4252
- // static) is still caught by the soft path; only the *producing* path is spared.
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
- return true;
4314
- }
4315
- function decideWedgeRestart(input, config2) {
4316
- if (!isWedgeCandidateCycle(input, config2)) return "none";
4317
- return input.consecutiveWedgeCycles >= config2.minCycles ? "wedged" : "none";
4318
- }
4319
-
4320
- // src/lib/wedge-poison-card.ts
4321
- var DEFAULTS2 = {
4322
- threshold: 3,
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
- function recordWedgeForCards(states, inProgressCardIds, nowMs, config2) {
4341
- if (inProgressCardIds.length === 0) {
4342
- return { next: new Map(states), newlyPoisoned: [] };
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 next = /* @__PURE__ */ new Map();
4345
- const newlyPoisoned = [];
4346
- for (const id of inProgressCardIds) {
4347
- if (next.has(id)) continue;
4348
- const count = (states.get(id)?.count ?? 0) + 1;
4349
- next.set(id, { count, lastWedgeAtMs: nowMs });
4350
- if (count === config2.threshold) newlyPoisoned.push(id);
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
- return { next, newlyPoisoned };
4229
+ agentInfoForDelivery.set(codeName, {
4230
+ agentDisplayName: displayName,
4231
+ ownerTeamName,
4232
+ resolverAgent,
4233
+ peopleByPersonId
4234
+ });
4353
4235
  }
4354
- function pruneCardStates(states, liveInProgressCardIds, nowMs, config2) {
4355
- const live = liveInProgressCardIds instanceof Set ? liveInProgressCardIds : new Set(liveInProgressCardIds);
4356
- const next = /* @__PURE__ */ new Map();
4357
- for (const [id, state7] of states) {
4358
- if (!live.has(id)) continue;
4359
- if (nowMs - state7.lastWedgeAtMs > config2.cooldownMs) continue;
4360
- next.set(id, state7);
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 isCardPoisoned(states, cardId, config2) {
4365
- return (states.get(cardId)?.count ?? 0) >= config2.threshold;
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 partitionActionableByPoison(actionable, states, config2) {
4368
- const allowed = [];
4369
- const suppressed = [];
4370
- for (const item of actionable) {
4371
- if (isCardPoisoned(states, item.id, config2)) suppressed.push(item);
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.28" : "dev";
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-34CY65FC.js");
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-KKWPOZSX.js");
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-KKWPOZSX.js");
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
- if (resolved.kind === "dm" && resolved.medium === "telegram") {
11117
- const tokens = agentChannelTokens.get(agentCodeName);
11118
- const botToken = tokens?.telegram;
11119
- if (!botToken) {
11120
- log(`[delivery] No Telegram bot token for '${agentCodeName}' \u2014 DM dropped`);
11121
- await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "TELEGRAM_NO_TOKEN", medium: "telegram" });
11122
- return;
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 result = await telegramApiCall(botToken, "sendMessage", {
11126
- chat_id: resolved.telegram_chat_id,
11127
- text: footeredBody
11128
- });
11129
- if (!result.ok) {
11130
- log(`[delivery] Telegram DM failed for '${agentCodeName}': ${result.description}`);
11131
- await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: `TELEGRAM_SEND_FAILED:${result.description ?? "unknown"}`, medium: "telegram" });
11132
- return;
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
- var agentInfoForDelivery = /* @__PURE__ */ new Map();
11143
- function cacheAgentDeliveryMetadata(codeName, refreshData) {
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
- const people = refreshData["people"] ?? [];
11177
- for (const p of people) {
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/schedules/delivery-status", {
11199
- agent_id: agentId,
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-UKOL6GWJ.js");
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,