@integrity-labs/agt-cli 0.12.8 → 0.13.0

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.
@@ -17,7 +17,7 @@ import {
17
17
  resolveChannels,
18
18
  resolveDmTarget,
19
19
  wrapScheduledTaskPrompt
20
- } from "../chunk-ZFTZDO5E.js";
20
+ } from "../chunk-WNYQKTBP.js";
21
21
  import {
22
22
  findTaskByTemplate,
23
23
  getProjectDir,
@@ -451,6 +451,192 @@ function parseExpiresAt(raw) {
451
451
  return null;
452
452
  }
453
453
 
454
+ // src/lib/channel-sweep.ts
455
+ import { execFileSync } from "child_process";
456
+ var CHANNEL_BASENAMES = [
457
+ "slack-channel",
458
+ "direct-chat-channel",
459
+ "telegram-channel"
460
+ ];
461
+ function parseEtime(s) {
462
+ const trimmed = s.trim();
463
+ if (!trimmed) return 0;
464
+ let days = 0;
465
+ let rest = trimmed;
466
+ const dashIdx = rest.indexOf("-");
467
+ if (dashIdx >= 0) {
468
+ days = parseInt(rest.slice(0, dashIdx), 10) || 0;
469
+ rest = rest.slice(dashIdx + 1);
470
+ }
471
+ const parts = rest.split(":").map((p) => parseInt(p, 10) || 0);
472
+ let h = 0;
473
+ let m = 0;
474
+ let sec = 0;
475
+ if (parts.length === 3) {
476
+ [h, m, sec] = parts;
477
+ } else if (parts.length === 2) {
478
+ [m, sec] = parts;
479
+ } else if (parts.length === 1) {
480
+ [sec] = parts;
481
+ }
482
+ return days * 86400 + h * 3600 + m * 60 + sec;
483
+ }
484
+ function parsePsOutput(psOutput) {
485
+ const results = [];
486
+ const lines = psOutput.split("\n");
487
+ for (const rawLine of lines) {
488
+ const line = rawLine.trimStart();
489
+ if (!line) continue;
490
+ const firstSpace = line.search(/\s+/);
491
+ if (firstSpace < 0) continue;
492
+ const pid = parseInt(line.slice(0, firstSpace), 10);
493
+ if (!Number.isFinite(pid)) continue;
494
+ const afterPid = line.slice(firstSpace).trimStart();
495
+ const secondSpace = afterPid.search(/\s+/);
496
+ if (secondSpace < 0) continue;
497
+ const ppid = parseInt(afterPid.slice(0, secondSpace), 10);
498
+ if (!Number.isFinite(ppid)) continue;
499
+ const afterPpid = afterPid.slice(secondSpace).trimStart();
500
+ const thirdSpace = afterPpid.search(/\s+/);
501
+ if (thirdSpace < 0) continue;
502
+ const etime = afterPpid.slice(0, thirdSpace);
503
+ const command = afterPpid.slice(thirdSpace).trimStart();
504
+ const channelMatch = command.match(
505
+ new RegExp(`(?:^|\\s)(?:[^\\s=]*/)?(${CHANNEL_BASENAMES.join("|")})\\.js(?:\\s|$)`)
506
+ );
507
+ const channelType = channelMatch?.[1];
508
+ if (!channelType) continue;
509
+ const match = command.match(/(?:^|\s)AGT_AGENT_CODE_NAME=([^\s]+)/);
510
+ if (!match) continue;
511
+ const codeName = match[1];
512
+ results.push({
513
+ pid,
514
+ ppid,
515
+ channelType,
516
+ codeName,
517
+ etimeSeconds: parseEtime(etime),
518
+ command: command.slice(0, 500)
519
+ });
520
+ }
521
+ return results;
522
+ }
523
+ function pickKillTargets(processes, agentCodeNames) {
524
+ const kills = [];
525
+ const ambiguousGroups = [];
526
+ const groups = /* @__PURE__ */ new Map();
527
+ for (const proc of processes) {
528
+ if (!agentCodeNames.has(proc.codeName)) continue;
529
+ const key = `${proc.codeName}:${proc.channelType}`;
530
+ const bucket = groups.get(key);
531
+ if (bucket) bucket.push(proc);
532
+ else groups.set(key, [proc]);
533
+ }
534
+ for (const [key, bucket] of groups) {
535
+ const orphans = bucket.filter((p) => p.ppid === 1);
536
+ const nonOrphans = bucket.filter((p) => p.ppid !== 1);
537
+ if (orphans.length === 0 && nonOrphans.length <= 1) continue;
538
+ if (nonOrphans.length > 1) {
539
+ ambiguousGroups.push(key);
540
+ continue;
541
+ }
542
+ for (const proc of orphans) {
543
+ kills.push({
544
+ pid: proc.pid,
545
+ codeName: proc.codeName,
546
+ channelType: proc.channelType,
547
+ etimeSeconds: proc.etimeSeconds,
548
+ reason: "orphan"
549
+ });
550
+ }
551
+ }
552
+ return { kills, ambiguousGroups };
553
+ }
554
+ function defaultKill(pid) {
555
+ try {
556
+ process.kill(pid, "SIGTERM");
557
+ } catch {
558
+ }
559
+ }
560
+ async function sweepChannelProcesses(opts) {
561
+ const { agentCodeNames, dryRun, log: log2 } = opts;
562
+ const kill = opts.killFn ?? defaultKill;
563
+ let psOutput = "";
564
+ try {
565
+ psOutput = execFileSync("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
566
+ encoding: "utf-8",
567
+ timeout: 5e3,
568
+ maxBuffer: 10 * 1024 * 1024
569
+ });
570
+ } catch (err) {
571
+ log2(`[channel-sweep] ps failed: ${err.message}`);
572
+ return { kills: [], inspected: 0, scannedAgents: [...agentCodeNames], dryRun };
573
+ }
574
+ const processes = parsePsOutput(psOutput);
575
+ const { kills, ambiguousGroups } = pickKillTargets(processes, agentCodeNames);
576
+ for (const target of kills) {
577
+ const ageMin = Math.round(target.etimeSeconds / 60);
578
+ log2(
579
+ `[channel-sweep]${dryRun ? "[dry-run]" : ""} surplus ${target.channelType} for ${target.codeName}: killing pid=${target.pid} (${target.reason}, age=${ageMin}m)`
580
+ );
581
+ if (!dryRun) kill(target.pid);
582
+ }
583
+ for (const group of ambiguousGroups) {
584
+ log2(
585
+ `[channel-sweep] ambiguous group ${group} \u2014 >1 non-orphan process, cannot determine which is live. Manual check recommended.`
586
+ );
587
+ }
588
+ return {
589
+ kills,
590
+ inspected: processes.length,
591
+ scannedAgents: [...agentCodeNames],
592
+ dryRun
593
+ };
594
+ }
595
+
596
+ // src/lib/delivery-hint.ts
597
+ var DEFAULT_PROBABILITY = 0.1;
598
+ function envSuffixFor(codeName) {
599
+ return codeName.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
600
+ }
601
+ function hintProbability(codeName, env = process.env) {
602
+ const suffix = codeName ? `__${envSuffixFor(codeName)}` : "";
603
+ if (suffix && env[`AGT_DELIVERY_HINT_DISABLED${suffix}`] === "1") return 0;
604
+ const perAgent = suffix ? env[`AGT_DELIVERY_HINT_PROBABILITY${suffix}`] : void 0;
605
+ if (perAgent != null) return clampProbability(perAgent);
606
+ if (env["AGT_DELIVERY_HINT_DISABLED"] === "1") return 0;
607
+ const host = env["AGT_DELIVERY_HINT_PROBABILITY"];
608
+ if (host != null) return clampProbability(host);
609
+ return DEFAULT_PROBABILITY;
610
+ }
611
+ function clampProbability(raw) {
612
+ const parsed = parseFloat(raw);
613
+ if (!Number.isFinite(parsed)) return DEFAULT_PROBABILITY;
614
+ if (parsed < 0) return 0;
615
+ if (parsed > 1) return 1;
616
+ return parsed;
617
+ }
618
+ function shouldIncludeHint(probability, rng = Math.random) {
619
+ if (probability <= 0) return false;
620
+ if (probability >= 1) return true;
621
+ return rng() < probability;
622
+ }
623
+ var HINT_VARIANTS = Object.freeze([
624
+ 'Quick note: you can change this scheduled task just by asking me \u2014 e.g. "Change the schedule for this task" or "Make this report less verbose in future".',
625
+ `By the way, this is on a schedule \u2014 if you'd like to tweak it, just say something like "run this weekly instead of daily" or "make this report less verbose in future" and I'll handle it.`,
626
+ 'Heads up: I deliver this on a schedule you can edit conversationally. Try "Change the schedule for this task" or "Make this report shorter" whenever you want to adjust it.',
627
+ `PS \u2014 no UI needed to tune this. Just tell me "Send this at 9am instead" or "Skip weekends for this report" and I'll update the schedule.`,
628
+ 'FYI this is a scheduled delivery. You can reshape it in plain English \u2014 e.g. "Make this fortnightly" or "Only include items from the last 24 hours".',
629
+ `You're the boss of this schedule \u2014 ask me things like "Pause this for two weeks" or "Include a summary at the top next time" and I'll apply it.`,
630
+ `Side note: you can say "Change this to Mondays only" or "Drop the preamble in future reports" and I'll update the task. No config screen required.`,
631
+ 'Reminder: I run this on a schedule you can edit by talking. Good openers: "Change when this fires" or "Make future reports more concise".',
632
+ `Small tip \u2014 this delivery is editable on the fly. Try "Move this to afternoons" or "Cut the detail down in future runs" and I'll retune it.`,
633
+ `If this cadence or format isn't quite right, just ask \u2014 "Only run this on weekdays" or "Shorter summaries from now on" both work, no form to fill out.`
634
+ ]);
635
+ function pickHintVariant(rng = Math.random) {
636
+ const idx = Math.floor(rng() * HINT_VARIANTS.length) % HINT_VARIANTS.length;
637
+ return HINT_VARIANTS[idx];
638
+ }
639
+
454
640
  // src/lib/realtime-chat.ts
455
641
  import { createClient } from "@supabase/supabase-js";
456
642
  var client = null;
@@ -736,6 +922,13 @@ var GATEWAY_PORT_STEP = 10;
736
922
  var GATEWAY_PORT_MAX = 18899;
737
923
  var AUGMENTED_DIR = join2(process.env["HOME"] ?? "/tmp", ".augmented");
738
924
  var GATEWAY_PORTS_FILE = join2(AUGMENTED_DIR, "gateway-ports.json");
925
+ var CHANNEL_SWEEP_INTERVAL_MS = (() => {
926
+ const raw = parseInt(process.env["AGT_CHANNEL_SWEEP_INTERVAL_MS"] ?? "", 10);
927
+ if (!Number.isFinite(raw)) return 5 * 60 * 1e3;
928
+ return Math.max(raw, 3e4);
929
+ })();
930
+ var CHANNEL_SWEEP_DRY_RUN = process.env["AGT_CHANNEL_SWEEP_DRY_RUN"] === "1";
931
+ var lastChannelSweepAt = 0;
739
932
  var config = null;
740
933
  var running = false;
741
934
  var pollTimer = null;
@@ -811,9 +1004,9 @@ function clearAgentCaches(agentId, codeName) {
811
1004
  var cachedFrameworkVersion = null;
812
1005
  var lastVersionCheckAt = 0;
813
1006
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
814
- function resolveBrewPath(execFileSync) {
1007
+ function resolveBrewPath(execFileSync2) {
815
1008
  try {
816
- const out = execFileSync("which", ["brew"], { timeout: 5e3 }).toString().trim();
1009
+ const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
817
1010
  if (out) return out;
818
1011
  } catch {
819
1012
  }
@@ -835,9 +1028,9 @@ async function ensureToolkitCli(toolkitSlug) {
835
1028
  }
836
1029
  const { binary, installer, package: pkg, script } = integration.cli_tool;
837
1030
  const resolvedInstaller = installer ?? "manual";
838
- const { execFileSync, execSync } = await import("child_process");
1031
+ const { execFileSync: execFileSync2, execSync } = await import("child_process");
839
1032
  try {
840
- execFileSync("which", [binary], { timeout: 5e3, stdio: "pipe" });
1033
+ execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
841
1034
  toolkitCliEnsured.add(toolkitSlug);
842
1035
  toolkitCliRetryAfter.delete(toolkitSlug);
843
1036
  return;
@@ -857,14 +1050,14 @@ async function ensureToolkitCli(toolkitSlug) {
857
1050
  return;
858
1051
  }
859
1052
  log(`[toolkit-install] ${toolkitSlug}: installing via npm (${pkg})\u2026`);
860
- execFileSync("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
1053
+ execFileSync2("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
861
1054
  } else if (resolvedInstaller === "brew") {
862
1055
  if (!pkg) {
863
1056
  log(`[toolkit-install] ${toolkitSlug}: installer=brew but no package declared`);
864
1057
  toolkitCliEnsured.add(toolkitSlug);
865
1058
  return;
866
1059
  }
867
- const brewPath = resolveBrewPath(execFileSync);
1060
+ const brewPath = resolveBrewPath(execFileSync2);
868
1061
  if (!brewPath) {
869
1062
  log(`[toolkit-install] ${toolkitSlug}: installer=brew but Homebrew not available \u2014 install manually: brew install ${pkg}`);
870
1063
  toolkitCliEnsured.add(toolkitSlug);
@@ -874,9 +1067,9 @@ async function ensureToolkitCli(toolkitSlug) {
874
1067
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
875
1068
  log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
876
1069
  if (isRoot) {
877
- execFileSync("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe" });
1070
+ execFileSync2("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe" });
878
1071
  } else {
879
- execFileSync(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
1072
+ execFileSync2(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
880
1073
  }
881
1074
  } else if (resolvedInstaller === "script") {
882
1075
  if (!script) {
@@ -897,7 +1090,7 @@ async function ensureToolkitCli(toolkitSlug) {
897
1090
  process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
898
1091
  }
899
1092
  try {
900
- execFileSync("which", [binary], { timeout: 5e3, stdio: "pipe" });
1093
+ execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
901
1094
  log(`[toolkit-install] ${toolkitSlug}: installed \u2014 ${binary} now on PATH`);
902
1095
  toolkitCliEnsured.add(toolkitSlug);
903
1096
  toolkitCliRetryAfter.delete(toolkitSlug);
@@ -910,8 +1103,8 @@ async function ensureFrameworkBinary(frameworkId) {
910
1103
  if (frameworkId !== "claude-code") return;
911
1104
  if (frameworkBinaryChecked.has(frameworkId)) return;
912
1105
  frameworkBinaryChecked.add(frameworkId);
913
- const { execFileSync } = await import("child_process");
914
- const brewPath = resolveBrewPath(execFileSync);
1106
+ const { execFileSync: execFileSync2 } = await import("child_process");
1107
+ const brewPath = resolveBrewPath(execFileSync2);
915
1108
  if (!brewPath) {
916
1109
  log("Homebrew not found (no `brew` on PATH, no /home/linuxbrew/.linuxbrew/bin/brew). Cannot auto-install Claude Code. Install manually: https://claude.ai/download");
917
1110
  return;
@@ -921,14 +1114,14 @@ async function ensureFrameworkBinary(frameworkId) {
921
1114
  const runBrew = (args, opts) => {
922
1115
  if (isRoot) {
923
1116
  const sudoArgs = ["-u", "ec2-user", "-H", brewPath, ...args];
924
- return execFileSync("sudo", sudoArgs, { ...opts, stdio: "pipe" }).toString();
1117
+ return execFileSync2("sudo", sudoArgs, { ...opts, stdio: "pipe" }).toString();
925
1118
  }
926
- return execFileSync(brewPath, args, { ...opts, stdio: "pipe" }).toString();
1119
+ return execFileSync2(brewPath, args, { ...opts, stdio: "pipe" }).toString();
927
1120
  };
928
1121
  let claudeExists = existsSync("/home/linuxbrew/.linuxbrew/bin/claude");
929
1122
  if (!claudeExists) {
930
1123
  try {
931
- execFileSync("which", ["claude"], { timeout: 5e3 });
1124
+ execFileSync2("which", ["claude"], { timeout: 5e3 });
932
1125
  claudeExists = true;
933
1126
  } catch {
934
1127
  }
@@ -981,20 +1174,20 @@ async function checkAndUpdateCli() {
981
1174
  if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
982
1175
  } catch {
983
1176
  }
984
- const { execFileSync } = await import("child_process");
1177
+ const { execFileSync: execFileSync2 } = await import("child_process");
985
1178
  let brewPath;
986
1179
  try {
987
- brewPath = execFileSync("which", ["brew"], { timeout: 5e3 }).toString().trim();
1180
+ brewPath = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
988
1181
  } catch {
989
1182
  return;
990
1183
  }
991
1184
  try {
992
- execFileSync(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
1185
+ execFileSync2(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
993
1186
  } catch (err) {
994
1187
  log(`[self-update] brew update failed (continuing with stale cache): ${err.message}`);
995
1188
  }
996
1189
  try {
997
- const outdated = execFileSync(brewPath, ["outdated", "--json=v2"], {
1190
+ const outdated = execFileSync2(brewPath, ["outdated", "--json=v2"], {
998
1191
  timeout: 3e4,
999
1192
  encoding: "utf-8"
1000
1193
  });
@@ -1005,7 +1198,7 @@ async function checkAndUpdateCli() {
1005
1198
  const latest = agtOutdated.current_version ?? "unknown";
1006
1199
  log(`[self-update] agt CLI update available: ${installed} \u2192 ${latest}. Upgrading...`);
1007
1200
  try {
1008
- execFileSync(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
1201
+ execFileSync2(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
1009
1202
  timeout: 12e4,
1010
1203
  stdio: "pipe"
1011
1204
  });
@@ -1591,6 +1784,17 @@ async function pollCycle() {
1591
1784
  }
1592
1785
  }
1593
1786
  await healthCheckGateways(agentStates);
1787
+ if (Date.now() - lastChannelSweepAt >= CHANNEL_SWEEP_INTERVAL_MS) {
1788
+ lastChannelSweepAt = Date.now();
1789
+ const agentCodeNames = new Set(agentStates.map((a) => a.codeName));
1790
+ sweepChannelProcesses({
1791
+ agentCodeNames,
1792
+ dryRun: CHANNEL_SWEEP_DRY_RUN,
1793
+ log
1794
+ }).catch((err) => {
1795
+ log(`[channel-sweep] sweep error: ${err.message}`);
1796
+ });
1797
+ }
1594
1798
  const lastHealthCheck = lastHarvestAt.get("__cron_health__") ?? 0;
1595
1799
  if (Date.now() - lastHealthCheck >= HARVEST_INTERVAL_MS) {
1596
1800
  lastHarvestAt.set("__cron_health__", Date.now());
@@ -4086,37 +4290,68 @@ async function sendSlackWebhookMessage(text) {
4086
4290
  }
4087
4291
  }
4088
4292
  async function sendSlackChannelMessage(agentCodeName, channelId, text) {
4293
+ const result = await postSlackChannelMessage(agentCodeName, channelId, text);
4294
+ return result.ok;
4295
+ }
4296
+ async function postSlackChannelMessage(agentCodeName, channelId, text, threadTs) {
4089
4297
  const botToken = agentChannelTokens.get(agentCodeName)?.slack;
4090
4298
  if (!botToken) {
4091
4299
  log(`No Slack bot token cached for '${agentCodeName}' \u2014 cannot post to ${channelId}`);
4092
- return false;
4300
+ return { ok: false };
4093
4301
  }
4094
4302
  try {
4095
4303
  const controller = new AbortController();
4096
4304
  const timeout = setTimeout(() => controller.abort(), 5e3);
4097
4305
  try {
4306
+ const body = { channel: channelId, text };
4307
+ if (threadTs) body.thread_ts = threadTs;
4098
4308
  const response = await fetch("https://slack.com/api/chat.postMessage", {
4099
4309
  method: "POST",
4100
4310
  headers: {
4101
4311
  "Authorization": `Bearer ${botToken}`,
4102
4312
  "Content-Type": "application/json"
4103
4313
  },
4104
- body: JSON.stringify({ channel: channelId, text }),
4314
+ body: JSON.stringify(body),
4105
4315
  signal: controller.signal
4106
4316
  });
4107
4317
  clearTimeout(timeout);
4108
4318
  const data = await response.json();
4109
4319
  if (!data.ok) {
4110
4320
  log(`Slack chat.postMessage failed for '${agentCodeName}' to ${channelId}: ${data.error}`);
4111
- return false;
4321
+ return { ok: false };
4112
4322
  }
4113
- return true;
4323
+ return { ok: true, ts: data.ts };
4114
4324
  } finally {
4115
4325
  clearTimeout(timeout);
4116
4326
  }
4117
4327
  } catch (err) {
4118
4328
  log(`Slack channel message error for '${agentCodeName}': ${err.message}`);
4119
- return false;
4329
+ return { ok: false };
4330
+ }
4331
+ }
4332
+ async function maybePostSlackThreadHint(agentCodeName, channelId, primaryTs) {
4333
+ if (!shouldIncludeHint(hintProbability(agentCodeName))) return;
4334
+ if (!primaryTs) {
4335
+ return;
4336
+ }
4337
+ const hint = pickHintVariant();
4338
+ const result = await postSlackChannelMessage(agentCodeName, channelId, hint, primaryTs);
4339
+ if (result.ok) {
4340
+ log(`[delivery-hint] Slack thread hint posted for '${agentCodeName}'`);
4341
+ }
4342
+ }
4343
+ async function maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId) {
4344
+ if (!shouldIncludeHint(hintProbability(agentCodeName))) return;
4345
+ const hint = pickHintVariant();
4346
+ try {
4347
+ const result = await telegramApiCall(botToken, "sendMessage", { chat_id: chatId, text: hint });
4348
+ if (!result.ok) {
4349
+ log(`[delivery-hint] Telegram follow-up failed for '${agentCodeName}': ${result.description ?? "unknown"}`);
4350
+ return;
4351
+ }
4352
+ log(`[delivery-hint] Telegram follow-up hint posted for '${agentCodeName}'`);
4353
+ } catch (err) {
4354
+ log(`[delivery-hint] Telegram follow-up failed for '${agentCodeName}': ${err.message}`);
4120
4355
  }
4121
4356
  }
4122
4357
  async function sendSlackAlert(alerts) {
@@ -4163,21 +4398,28 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
4163
4398
  }
4164
4399
  if (parsed.kind === "channel") {
4165
4400
  if (parsed.provider === "slack") {
4166
- const sent = await sendSlackChannelMessage(agentCodeName, parsed.channel_id ?? "", body);
4401
+ const channelId = parsed.channel_id ?? "";
4402
+ const sent = await postSlackChannelMessage(agentCodeName, channelId, body);
4167
4403
  await reportDeliveryStatus(agentId, taskId, {
4168
- status: sent ? "ok" : "failed",
4404
+ status: sent.ok ? "ok" : "failed",
4169
4405
  medium: "slack",
4170
- error_code: sent ? null : "SLACK_SEND_FAILED"
4406
+ error_code: sent.ok ? null : "SLACK_SEND_FAILED"
4171
4407
  });
4408
+ if (sent.ok) await maybePostSlackThreadHint(agentCodeName, channelId, sent.ts);
4172
4409
  return;
4173
4410
  }
4174
- const toStr = `chat:${parsed.chat_id ?? ""}`;
4411
+ const chatId = parsed.chat_id ?? "";
4412
+ const toStr = `chat:${chatId}`;
4175
4413
  const result = await sendTaskNotification(agentCodeName, "telegram", toStr, body);
4176
4414
  await reportDeliveryStatus(agentId, taskId, {
4177
4415
  status: result.ok ? "ok" : "failed",
4178
4416
  medium: "telegram",
4179
4417
  error_code: result.ok ? null : result.error_code ?? "TELEGRAM_SEND_FAILED"
4180
4418
  });
4419
+ if (result.ok) {
4420
+ const botToken = agentChannelTokens.get(agentCodeName)?.telegram;
4421
+ if (botToken) await maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId);
4422
+ }
4181
4423
  return;
4182
4424
  }
4183
4425
  const agentRow = agentInfoForDelivery.get(agentCodeName);
@@ -4229,12 +4471,13 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
4229
4471
  await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: errCode, medium: "slack" });
4230
4472
  return;
4231
4473
  }
4232
- const sent = await sendSlackChannelMessage(agentCodeName, openJson.channel.id, footeredBody);
4474
+ const sent = await postSlackChannelMessage(agentCodeName, openJson.channel.id, footeredBody);
4233
4475
  await reportDeliveryStatus(agentId, taskId, {
4234
- status: sent ? "ok" : "failed",
4476
+ status: sent.ok ? "ok" : "failed",
4235
4477
  medium: "slack",
4236
- error_code: sent ? null : "SLACK_SEND_FAILED"
4478
+ error_code: sent.ok ? null : "SLACK_SEND_FAILED"
4237
4479
  });
4480
+ if (sent.ok) await maybePostSlackThreadHint(agentCodeName, openJson.channel.id, sent.ts);
4238
4481
  } catch (err) {
4239
4482
  const isAbort = err.name === "AbortError";
4240
4483
  const errCode = isAbort ? "SLACK_OPEN_TIMEOUT" : "SLACK_EXCEPTION";
@@ -4262,6 +4505,7 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
4262
4505
  return;
4263
4506
  }
4264
4507
  await reportDeliveryStatus(agentId, taskId, { status: "ok", medium: "telegram" });
4508
+ await maybeSendTelegramFollowUpHint(agentCodeName, botToken, resolved.telegram_chat_id);
4265
4509
  } catch (err) {
4266
4510
  log(`[delivery] Telegram DM exception for '${agentCodeName}': ${err.message}`);
4267
4511
  await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "TELEGRAM_EXCEPTION", medium: "telegram" });