@integrity-labs/agt-cli 0.12.9 → 0.14.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.
@@ -9,6 +9,7 @@ import {
9
9
  getIntegration,
10
10
  isParseError,
11
11
  isResolveError,
12
+ isSuppressOutput,
12
13
  parseDeliveryTarget,
13
14
  provision,
14
15
  provisionIsolationHook,
@@ -17,7 +18,7 @@ import {
17
18
  resolveChannels,
18
19
  resolveDmTarget,
19
20
  wrapScheduledTaskPrompt
20
- } from "../chunk-ZFTZDO5E.js";
21
+ } from "../chunk-Y2ZGJIXI.js";
21
22
  import {
22
23
  findTaskByTemplate,
23
24
  getProjectDir,
@@ -451,6 +452,237 @@ function parseExpiresAt(raw) {
451
452
  return null;
452
453
  }
453
454
 
455
+ // src/lib/channel-sweep.ts
456
+ import { execFileSync } from "child_process";
457
+ var CHANNEL_BASENAMES = [
458
+ "slack-channel",
459
+ "direct-chat-channel",
460
+ "telegram-channel"
461
+ ];
462
+ function parseEtime(s) {
463
+ const trimmed = s.trim();
464
+ if (!trimmed) return 0;
465
+ let days = 0;
466
+ let rest = trimmed;
467
+ const dashIdx = rest.indexOf("-");
468
+ if (dashIdx >= 0) {
469
+ days = parseInt(rest.slice(0, dashIdx), 10) || 0;
470
+ rest = rest.slice(dashIdx + 1);
471
+ }
472
+ const parts = rest.split(":").map((p) => parseInt(p, 10) || 0);
473
+ let h = 0;
474
+ let m = 0;
475
+ let sec = 0;
476
+ if (parts.length === 3) {
477
+ [h, m, sec] = parts;
478
+ } else if (parts.length === 2) {
479
+ [m, sec] = parts;
480
+ } else if (parts.length === 1) {
481
+ [sec] = parts;
482
+ }
483
+ return days * 86400 + h * 3600 + m * 60 + sec;
484
+ }
485
+ function parsePsOutput(psOutput) {
486
+ const results = [];
487
+ const lines = psOutput.split("\n");
488
+ for (const rawLine of lines) {
489
+ const line = rawLine.trimStart();
490
+ if (!line) continue;
491
+ const firstSpace = line.search(/\s+/);
492
+ if (firstSpace < 0) continue;
493
+ const pid = parseInt(line.slice(0, firstSpace), 10);
494
+ if (!Number.isFinite(pid)) continue;
495
+ const afterPid = line.slice(firstSpace).trimStart();
496
+ const secondSpace = afterPid.search(/\s+/);
497
+ if (secondSpace < 0) continue;
498
+ const ppid = parseInt(afterPid.slice(0, secondSpace), 10);
499
+ if (!Number.isFinite(ppid)) continue;
500
+ const afterPpid = afterPid.slice(secondSpace).trimStart();
501
+ const thirdSpace = afterPpid.search(/\s+/);
502
+ if (thirdSpace < 0) continue;
503
+ const etime = afterPpid.slice(0, thirdSpace);
504
+ const command = afterPpid.slice(thirdSpace).trimStart();
505
+ const channelMatch = command.match(
506
+ new RegExp(`(?:^|\\s)(?:[^\\s=]*/)?(${CHANNEL_BASENAMES.join("|")})\\.js(?:\\s|$)`)
507
+ );
508
+ const channelType = channelMatch?.[1];
509
+ if (!channelType) continue;
510
+ const match = command.match(/(?:^|\s)AGT_AGENT_CODE_NAME=([^\s]+)/);
511
+ if (!match) continue;
512
+ const codeName = match[1];
513
+ results.push({
514
+ pid,
515
+ ppid,
516
+ channelType,
517
+ codeName,
518
+ etimeSeconds: parseEtime(etime),
519
+ command: command.slice(0, 500)
520
+ });
521
+ }
522
+ return results;
523
+ }
524
+ function pickKillTargets(processes, agentCodeNames) {
525
+ const kills = [];
526
+ const ambiguousGroups = [];
527
+ const groups = /* @__PURE__ */ new Map();
528
+ for (const proc of processes) {
529
+ if (!agentCodeNames.has(proc.codeName)) continue;
530
+ const key = `${proc.codeName}:${proc.channelType}`;
531
+ const bucket = groups.get(key);
532
+ if (bucket) bucket.push(proc);
533
+ else groups.set(key, [proc]);
534
+ }
535
+ for (const [key, bucket] of groups) {
536
+ const orphans = bucket.filter((p) => p.ppid === 1);
537
+ const nonOrphans = bucket.filter((p) => p.ppid !== 1);
538
+ if (orphans.length === 0 && nonOrphans.length <= 1) continue;
539
+ if (nonOrphans.length > 1) {
540
+ ambiguousGroups.push(key);
541
+ continue;
542
+ }
543
+ for (const proc of orphans) {
544
+ kills.push({
545
+ pid: proc.pid,
546
+ codeName: proc.codeName,
547
+ channelType: proc.channelType,
548
+ etimeSeconds: proc.etimeSeconds,
549
+ reason: "orphan"
550
+ });
551
+ }
552
+ }
553
+ return { kills, ambiguousGroups };
554
+ }
555
+ function defaultKill(pid) {
556
+ try {
557
+ process.kill(pid, "SIGTERM");
558
+ } catch {
559
+ }
560
+ }
561
+ async function sweepChannelProcesses(opts) {
562
+ const { agentCodeNames, dryRun, log: log2 } = opts;
563
+ const kill = opts.killFn ?? defaultKill;
564
+ let psOutput = "";
565
+ try {
566
+ psOutput = execFileSync("ps", ["eww", "-o", "pid=,ppid=,etime=,command="], {
567
+ encoding: "utf-8",
568
+ timeout: 5e3,
569
+ maxBuffer: 10 * 1024 * 1024
570
+ });
571
+ } catch (err) {
572
+ log2(`[channel-sweep] ps failed: ${err.message}`);
573
+ return { kills: [], inspected: 0, scannedAgents: [...agentCodeNames], dryRun };
574
+ }
575
+ const processes = parsePsOutput(psOutput);
576
+ const { kills, ambiguousGroups } = pickKillTargets(processes, agentCodeNames);
577
+ for (const target of kills) {
578
+ const ageMin = Math.round(target.etimeSeconds / 60);
579
+ log2(
580
+ `[channel-sweep]${dryRun ? "[dry-run]" : ""} surplus ${target.channelType} for ${target.codeName}: killing pid=${target.pid} (${target.reason}, age=${ageMin}m)`
581
+ );
582
+ if (!dryRun) kill(target.pid);
583
+ }
584
+ for (const group of ambiguousGroups) {
585
+ log2(
586
+ `[channel-sweep] ambiguous group ${group} \u2014 >1 non-orphan process, cannot determine which is live. Manual check recommended.`
587
+ );
588
+ }
589
+ return {
590
+ kills,
591
+ inspected: processes.length,
592
+ scannedAgents: [...agentCodeNames],
593
+ dryRun
594
+ };
595
+ }
596
+
597
+ // src/lib/delivery-hint.ts
598
+ var DEFAULT_PROBABILITY = 0.1;
599
+ function envSuffixFor(codeName) {
600
+ return codeName.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
601
+ }
602
+ function hintProbability(codeName, env = process.env) {
603
+ const suffix = codeName ? `__${envSuffixFor(codeName)}` : "";
604
+ if (suffix && env[`AGT_DELIVERY_HINT_DISABLED${suffix}`] === "1") return 0;
605
+ const perAgent = suffix ? env[`AGT_DELIVERY_HINT_PROBABILITY${suffix}`] : void 0;
606
+ if (perAgent != null) return clampProbability(perAgent);
607
+ if (env["AGT_DELIVERY_HINT_DISABLED"] === "1") return 0;
608
+ const host = env["AGT_DELIVERY_HINT_PROBABILITY"];
609
+ if (host != null) return clampProbability(host);
610
+ return DEFAULT_PROBABILITY;
611
+ }
612
+ function clampProbability(raw) {
613
+ const parsed = parseFloat(raw);
614
+ if (!Number.isFinite(parsed)) return DEFAULT_PROBABILITY;
615
+ if (parsed < 0) return 0;
616
+ if (parsed > 1) return 1;
617
+ return parsed;
618
+ }
619
+ function shouldIncludeHint(probability, rng = Math.random) {
620
+ if (probability <= 0) return false;
621
+ if (probability >= 1) return true;
622
+ return rng() < probability;
623
+ }
624
+ var HINT_VARIANTS = Object.freeze([
625
+ '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".',
626
+ `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.`,
627
+ '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.',
628
+ `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.`,
629
+ '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".',
630
+ `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.`,
631
+ `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.`,
632
+ 'Reminder: I run this on a schedule you can edit by talking. Good openers: "Change when this fires" or "Make future reports more concise".',
633
+ `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.`,
634
+ `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.`
635
+ ]);
636
+ function pickHintVariant(rng = Math.random) {
637
+ const idx = Math.floor(rng() * HINT_VARIANTS.length) % HINT_VARIANTS.length;
638
+ return HINT_VARIANTS[idx];
639
+ }
640
+
641
+ // src/lib/delivery-schedule-link.ts
642
+ function envSuffixFor2(codeName) {
643
+ return codeName.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
644
+ }
645
+ function scheduleLinkFooterEnabled(codeName, env = process.env) {
646
+ if (codeName) {
647
+ const perAgent = env[`AGT_SCHEDULE_LINK_FOOTER_DISABLED__${envSuffixFor2(codeName)}`];
648
+ if (perAgent === "1") return false;
649
+ }
650
+ if (env["AGT_SCHEDULE_LINK_FOOTER_DISABLED"] === "1") return false;
651
+ return true;
652
+ }
653
+ function getConsoleUrl(env = process.env) {
654
+ const canonical = env["AGT_CONSOLE_URL"]?.trim();
655
+ if (canonical) return canonical.replace(/\/+$/, "");
656
+ const fallback = env["NEXT_PUBLIC_APP_URL"]?.trim();
657
+ if (fallback) return fallback.replace(/\/+$/, "");
658
+ return null;
659
+ }
660
+ function buildScheduleEditLink(consoleUrl, agentId, taskId) {
661
+ const base = consoleUrl.replace(/\/+$/, "");
662
+ return `${base}/agents/${encodeURIComponent(agentId)}/schedules/${encodeURIComponent(taskId)}`;
663
+ }
664
+ function formatScheduleLinkFooter(url, medium) {
665
+ if (medium === "slack") return `<${url}|Edit schedule>`;
666
+ if (medium === "telegram") return `Edit schedule: ${url}`;
667
+ return url;
668
+ }
669
+ function appendScheduleLinkFooter(body, url, medium) {
670
+ const footer = formatScheduleLinkFooter(url, medium);
671
+ const trimmed = body.replace(/\s+$/, "");
672
+ if (trimmed.endsWith(footer)) return trimmed;
673
+ return `${trimmed}
674
+
675
+ ${footer}`;
676
+ }
677
+ function withScheduleLinkFooter(opts) {
678
+ if (!opts.taskId) return opts.body;
679
+ if (!scheduleLinkFooterEnabled(opts.codeName, opts.env)) return opts.body;
680
+ const consoleUrl = getConsoleUrl(opts.env);
681
+ if (!consoleUrl) return opts.body;
682
+ const url = buildScheduleEditLink(consoleUrl, opts.agentId, opts.taskId);
683
+ return appendScheduleLinkFooter(opts.body, url, opts.medium);
684
+ }
685
+
454
686
  // src/lib/realtime-chat.ts
455
687
  import { createClient } from "@supabase/supabase-js";
456
688
  var client = null;
@@ -736,6 +968,13 @@ var GATEWAY_PORT_STEP = 10;
736
968
  var GATEWAY_PORT_MAX = 18899;
737
969
  var AUGMENTED_DIR = join2(process.env["HOME"] ?? "/tmp", ".augmented");
738
970
  var GATEWAY_PORTS_FILE = join2(AUGMENTED_DIR, "gateway-ports.json");
971
+ var CHANNEL_SWEEP_INTERVAL_MS = (() => {
972
+ const raw = parseInt(process.env["AGT_CHANNEL_SWEEP_INTERVAL_MS"] ?? "", 10);
973
+ if (!Number.isFinite(raw)) return 5 * 60 * 1e3;
974
+ return Math.max(raw, 3e4);
975
+ })();
976
+ var CHANNEL_SWEEP_DRY_RUN = process.env["AGT_CHANNEL_SWEEP_DRY_RUN"] === "1";
977
+ var lastChannelSweepAt = 0;
739
978
  var config = null;
740
979
  var running = false;
741
980
  var pollTimer = null;
@@ -811,9 +1050,9 @@ function clearAgentCaches(agentId, codeName) {
811
1050
  var cachedFrameworkVersion = null;
812
1051
  var lastVersionCheckAt = 0;
813
1052
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
814
- function resolveBrewPath(execFileSync) {
1053
+ function resolveBrewPath(execFileSync2) {
815
1054
  try {
816
- const out = execFileSync("which", ["brew"], { timeout: 5e3 }).toString().trim();
1055
+ const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
817
1056
  if (out) return out;
818
1057
  } catch {
819
1058
  }
@@ -835,9 +1074,9 @@ async function ensureToolkitCli(toolkitSlug) {
835
1074
  }
836
1075
  const { binary, installer, package: pkg, script } = integration.cli_tool;
837
1076
  const resolvedInstaller = installer ?? "manual";
838
- const { execFileSync, execSync } = await import("child_process");
1077
+ const { execFileSync: execFileSync2, execSync } = await import("child_process");
839
1078
  try {
840
- execFileSync("which", [binary], { timeout: 5e3, stdio: "pipe" });
1079
+ execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
841
1080
  toolkitCliEnsured.add(toolkitSlug);
842
1081
  toolkitCliRetryAfter.delete(toolkitSlug);
843
1082
  return;
@@ -857,14 +1096,14 @@ async function ensureToolkitCli(toolkitSlug) {
857
1096
  return;
858
1097
  }
859
1098
  log(`[toolkit-install] ${toolkitSlug}: installing via npm (${pkg})\u2026`);
860
- execFileSync("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
1099
+ execFileSync2("npm", ["install", "-g", pkg], { timeout: 18e4, stdio: "pipe" });
861
1100
  } else if (resolvedInstaller === "brew") {
862
1101
  if (!pkg) {
863
1102
  log(`[toolkit-install] ${toolkitSlug}: installer=brew but no package declared`);
864
1103
  toolkitCliEnsured.add(toolkitSlug);
865
1104
  return;
866
1105
  }
867
- const brewPath = resolveBrewPath(execFileSync);
1106
+ const brewPath = resolveBrewPath(execFileSync2);
868
1107
  if (!brewPath) {
869
1108
  log(`[toolkit-install] ${toolkitSlug}: installer=brew but Homebrew not available \u2014 install manually: brew install ${pkg}`);
870
1109
  toolkitCliEnsured.add(toolkitSlug);
@@ -874,9 +1113,9 @@ async function ensureToolkitCli(toolkitSlug) {
874
1113
  const isRoot = typeof process.getuid === "function" && process.getuid() === 0;
875
1114
  log(`[toolkit-install] ${toolkitSlug}: installing via brew (${pkg})\u2026`);
876
1115
  if (isRoot) {
877
- execFileSync("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe" });
1116
+ execFileSync2("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe" });
878
1117
  } else {
879
- execFileSync(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
1118
+ execFileSync2(brewPath, ["install", pkg], { timeout: 18e4, stdio: "pipe" });
880
1119
  }
881
1120
  } else if (resolvedInstaller === "script") {
882
1121
  if (!script) {
@@ -897,7 +1136,7 @@ async function ensureToolkitCli(toolkitSlug) {
897
1136
  process.env.PATH = `${brewBinDir}:${process.env.PATH ?? ""}`;
898
1137
  }
899
1138
  try {
900
- execFileSync("which", [binary], { timeout: 5e3, stdio: "pipe" });
1139
+ execFileSync2("which", [binary], { timeout: 5e3, stdio: "pipe" });
901
1140
  log(`[toolkit-install] ${toolkitSlug}: installed \u2014 ${binary} now on PATH`);
902
1141
  toolkitCliEnsured.add(toolkitSlug);
903
1142
  toolkitCliRetryAfter.delete(toolkitSlug);
@@ -910,8 +1149,8 @@ async function ensureFrameworkBinary(frameworkId) {
910
1149
  if (frameworkId !== "claude-code") return;
911
1150
  if (frameworkBinaryChecked.has(frameworkId)) return;
912
1151
  frameworkBinaryChecked.add(frameworkId);
913
- const { execFileSync } = await import("child_process");
914
- const brewPath = resolveBrewPath(execFileSync);
1152
+ const { execFileSync: execFileSync2 } = await import("child_process");
1153
+ const brewPath = resolveBrewPath(execFileSync2);
915
1154
  if (!brewPath) {
916
1155
  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
1156
  return;
@@ -921,14 +1160,14 @@ async function ensureFrameworkBinary(frameworkId) {
921
1160
  const runBrew = (args, opts) => {
922
1161
  if (isRoot) {
923
1162
  const sudoArgs = ["-u", "ec2-user", "-H", brewPath, ...args];
924
- return execFileSync("sudo", sudoArgs, { ...opts, stdio: "pipe" }).toString();
1163
+ return execFileSync2("sudo", sudoArgs, { ...opts, stdio: "pipe" }).toString();
925
1164
  }
926
- return execFileSync(brewPath, args, { ...opts, stdio: "pipe" }).toString();
1165
+ return execFileSync2(brewPath, args, { ...opts, stdio: "pipe" }).toString();
927
1166
  };
928
1167
  let claudeExists = existsSync("/home/linuxbrew/.linuxbrew/bin/claude");
929
1168
  if (!claudeExists) {
930
1169
  try {
931
- execFileSync("which", ["claude"], { timeout: 5e3 });
1170
+ execFileSync2("which", ["claude"], { timeout: 5e3 });
932
1171
  claudeExists = true;
933
1172
  } catch {
934
1173
  }
@@ -981,20 +1220,20 @@ async function checkAndUpdateCli() {
981
1220
  if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return;
982
1221
  } catch {
983
1222
  }
984
- const { execFileSync } = await import("child_process");
1223
+ const { execFileSync: execFileSync2 } = await import("child_process");
985
1224
  let brewPath;
986
1225
  try {
987
- brewPath = execFileSync("which", ["brew"], { timeout: 5e3 }).toString().trim();
1226
+ brewPath = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
988
1227
  } catch {
989
1228
  return;
990
1229
  }
991
1230
  try {
992
- execFileSync(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
1231
+ execFileSync2(brewPath, ["update", "--quiet"], { timeout: 6e4, stdio: "pipe" });
993
1232
  } catch (err) {
994
1233
  log(`[self-update] brew update failed (continuing with stale cache): ${err.message}`);
995
1234
  }
996
1235
  try {
997
- const outdated = execFileSync(brewPath, ["outdated", "--json=v2"], {
1236
+ const outdated = execFileSync2(brewPath, ["outdated", "--json=v2"], {
998
1237
  timeout: 3e4,
999
1238
  encoding: "utf-8"
1000
1239
  });
@@ -1005,7 +1244,7 @@ async function checkAndUpdateCli() {
1005
1244
  const latest = agtOutdated.current_version ?? "unknown";
1006
1245
  log(`[self-update] agt CLI update available: ${installed} \u2192 ${latest}. Upgrading...`);
1007
1246
  try {
1008
- execFileSync(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
1247
+ execFileSync2(brewPath, ["upgrade", "integrity-labs/tap/agt"], {
1009
1248
  timeout: 12e4,
1010
1249
  stdio: "pipe"
1011
1250
  });
@@ -1591,6 +1830,17 @@ async function pollCycle() {
1591
1830
  }
1592
1831
  }
1593
1832
  await healthCheckGateways(agentStates);
1833
+ if (Date.now() - lastChannelSweepAt >= CHANNEL_SWEEP_INTERVAL_MS) {
1834
+ lastChannelSweepAt = Date.now();
1835
+ const agentCodeNames = new Set(agentStates.map((a) => a.codeName));
1836
+ sweepChannelProcesses({
1837
+ agentCodeNames,
1838
+ dryRun: CHANNEL_SWEEP_DRY_RUN,
1839
+ log
1840
+ }).catch((err) => {
1841
+ log(`[channel-sweep] sweep error: ${err.message}`);
1842
+ });
1843
+ }
1594
1844
  const lastHealthCheck = lastHarvestAt.get("__cron_health__") ?? 0;
1595
1845
  if (Date.now() - lastHealthCheck >= HARVEST_INTERVAL_MS) {
1596
1846
  lastHarvestAt.set("__cron_health__", Date.now());
@@ -2958,6 +3208,16 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
2958
3208
  }
2959
3209
  async function processClaudeTaskResult(codeName, agentId, templateId, output, delivery) {
2960
3210
  try {
3211
+ if (isSuppressOutput(output)) {
3212
+ log(`[claude-scheduler] Suppressing delivery for '${codeName}' \u2014 output matched no-delivery sentinel or was empty`);
3213
+ if (delivery?.mode === "announce" && delivery.to) {
3214
+ await reportDeliveryStatus(agentId, delivery.taskId, {
3215
+ status: "skipped",
3216
+ error_code: "NO_CONTENT"
3217
+ });
3218
+ }
3219
+ return;
3220
+ }
2961
3221
  if (STANDUP_TEMPLATES.has(templateId)) {
2962
3222
  const standup = parseStandupSummary(output);
2963
3223
  await api.post("/host/agent-status", {
@@ -2991,7 +3251,7 @@ async function processClaudeTaskResult(codeName, agentId, templateId, output, de
2991
3251
  log(`[claude-scheduler] Kanban updates posted for '${codeName}' (${kanbanUpdates.length} updates)`);
2992
3252
  }
2993
3253
  }
2994
- if (delivery?.mode === "announce" && delivery.to && output) {
3254
+ if (delivery?.mode === "announce" && delivery.to) {
2995
3255
  await deliverScheduledTaskOutput(
2996
3256
  codeName,
2997
3257
  agentId,
@@ -4086,37 +4346,68 @@ async function sendSlackWebhookMessage(text) {
4086
4346
  }
4087
4347
  }
4088
4348
  async function sendSlackChannelMessage(agentCodeName, channelId, text) {
4349
+ const result = await postSlackChannelMessage(agentCodeName, channelId, text);
4350
+ return result.ok;
4351
+ }
4352
+ async function postSlackChannelMessage(agentCodeName, channelId, text, threadTs) {
4089
4353
  const botToken = agentChannelTokens.get(agentCodeName)?.slack;
4090
4354
  if (!botToken) {
4091
4355
  log(`No Slack bot token cached for '${agentCodeName}' \u2014 cannot post to ${channelId}`);
4092
- return false;
4356
+ return { ok: false };
4093
4357
  }
4094
4358
  try {
4095
4359
  const controller = new AbortController();
4096
4360
  const timeout = setTimeout(() => controller.abort(), 5e3);
4097
4361
  try {
4362
+ const body = { channel: channelId, text };
4363
+ if (threadTs) body.thread_ts = threadTs;
4098
4364
  const response = await fetch("https://slack.com/api/chat.postMessage", {
4099
4365
  method: "POST",
4100
4366
  headers: {
4101
4367
  "Authorization": `Bearer ${botToken}`,
4102
4368
  "Content-Type": "application/json"
4103
4369
  },
4104
- body: JSON.stringify({ channel: channelId, text }),
4370
+ body: JSON.stringify(body),
4105
4371
  signal: controller.signal
4106
4372
  });
4107
4373
  clearTimeout(timeout);
4108
4374
  const data = await response.json();
4109
4375
  if (!data.ok) {
4110
4376
  log(`Slack chat.postMessage failed for '${agentCodeName}' to ${channelId}: ${data.error}`);
4111
- return false;
4377
+ return { ok: false };
4112
4378
  }
4113
- return true;
4379
+ return { ok: true, ts: data.ts };
4114
4380
  } finally {
4115
4381
  clearTimeout(timeout);
4116
4382
  }
4117
4383
  } catch (err) {
4118
4384
  log(`Slack channel message error for '${agentCodeName}': ${err.message}`);
4119
- return false;
4385
+ return { ok: false };
4386
+ }
4387
+ }
4388
+ async function maybePostSlackThreadHint(agentCodeName, channelId, primaryTs) {
4389
+ if (!shouldIncludeHint(hintProbability(agentCodeName))) return;
4390
+ if (!primaryTs) {
4391
+ return;
4392
+ }
4393
+ const hint = pickHintVariant();
4394
+ const result = await postSlackChannelMessage(agentCodeName, channelId, hint, primaryTs);
4395
+ if (result.ok) {
4396
+ log(`[delivery-hint] Slack thread hint posted for '${agentCodeName}'`);
4397
+ }
4398
+ }
4399
+ async function maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId) {
4400
+ if (!shouldIncludeHint(hintProbability(agentCodeName))) return;
4401
+ const hint = pickHintVariant();
4402
+ try {
4403
+ const result = await telegramApiCall(botToken, "sendMessage", { chat_id: chatId, text: hint });
4404
+ if (!result.ok) {
4405
+ log(`[delivery-hint] Telegram follow-up failed for '${agentCodeName}': ${result.description ?? "unknown"}`);
4406
+ return;
4407
+ }
4408
+ log(`[delivery-hint] Telegram follow-up hint posted for '${agentCodeName}'`);
4409
+ } catch (err) {
4410
+ log(`[delivery-hint] Telegram follow-up failed for '${agentCodeName}': ${err.message}`);
4120
4411
  }
4121
4412
  }
4122
4413
  async function sendSlackAlert(alerts) {
@@ -4132,9 +4423,10 @@ ${a.detail}`;
4132
4423
  ${blocks.join("\n\n")}`);
4133
4424
  }
4134
4425
  async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, body, taskId) {
4426
+ const withLink = (b, medium) => withScheduleLinkFooter({ body: b, medium, codeName: agentCodeName, agentId, taskId });
4135
4427
  if (typeof rawTarget === "string") {
4136
4428
  if (rawTarget.startsWith("channel:")) {
4137
- const result = await sendTaskNotification(agentCodeName, "slack", rawTarget, body);
4429
+ const result = await sendTaskNotification(agentCodeName, "slack", rawTarget, withLink(body, "slack"));
4138
4430
  await reportDeliveryStatus(agentId, taskId, {
4139
4431
  status: result.ok ? "ok" : "failed",
4140
4432
  medium: "slack",
@@ -4143,7 +4435,7 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
4143
4435
  return;
4144
4436
  }
4145
4437
  if (rawTarget.startsWith("chat:")) {
4146
- const result = await sendTaskNotification(agentCodeName, "telegram", rawTarget, body);
4438
+ const result = await sendTaskNotification(agentCodeName, "telegram", rawTarget, withLink(body, "telegram"));
4147
4439
  await reportDeliveryStatus(agentId, taskId, {
4148
4440
  status: result.ok ? "ok" : "failed",
4149
4441
  medium: "telegram",
@@ -4163,21 +4455,28 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
4163
4455
  }
4164
4456
  if (parsed.kind === "channel") {
4165
4457
  if (parsed.provider === "slack") {
4166
- const sent = await sendSlackChannelMessage(agentCodeName, parsed.channel_id ?? "", body);
4458
+ const channelId = parsed.channel_id ?? "";
4459
+ const sent = await postSlackChannelMessage(agentCodeName, channelId, withLink(body, "slack"));
4167
4460
  await reportDeliveryStatus(agentId, taskId, {
4168
- status: sent ? "ok" : "failed",
4461
+ status: sent.ok ? "ok" : "failed",
4169
4462
  medium: "slack",
4170
- error_code: sent ? null : "SLACK_SEND_FAILED"
4463
+ error_code: sent.ok ? null : "SLACK_SEND_FAILED"
4171
4464
  });
4465
+ if (sent.ok) await maybePostSlackThreadHint(agentCodeName, channelId, sent.ts);
4172
4466
  return;
4173
4467
  }
4174
- const toStr = `chat:${parsed.chat_id ?? ""}`;
4175
- const result = await sendTaskNotification(agentCodeName, "telegram", toStr, body);
4468
+ const chatId = parsed.chat_id ?? "";
4469
+ const toStr = `chat:${chatId}`;
4470
+ const result = await sendTaskNotification(agentCodeName, "telegram", toStr, withLink(body, "telegram"));
4176
4471
  await reportDeliveryStatus(agentId, taskId, {
4177
4472
  status: result.ok ? "ok" : "failed",
4178
4473
  medium: "telegram",
4179
4474
  error_code: result.ok ? null : result.error_code ?? "TELEGRAM_SEND_FAILED"
4180
4475
  });
4476
+ if (result.ok) {
4477
+ const botToken = agentChannelTokens.get(agentCodeName)?.telegram;
4478
+ if (botToken) await maybeSendTelegramFollowUpHint(agentCodeName, botToken, chatId);
4479
+ }
4181
4480
  return;
4182
4481
  }
4183
4482
  const agentRow = agentInfoForDelivery.get(agentCodeName);
@@ -4192,11 +4491,12 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
4192
4491
  await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: resolved.code });
4193
4492
  return;
4194
4493
  }
4195
- const footeredBody = appendDmFooter(
4494
+ const attributionBody = appendDmFooter(
4196
4495
  body,
4197
4496
  agentRow.ownerTeamName,
4198
4497
  agentRow.agentDisplayName
4199
4498
  );
4499
+ const footeredBody = resolved.kind === "dm" ? withLink(attributionBody, resolved.medium === "slack" ? "slack" : "telegram") : attributionBody;
4200
4500
  if (resolved.kind === "dm" && resolved.medium === "slack") {
4201
4501
  const tokens = agentChannelTokens.get(agentCodeName);
4202
4502
  const botToken = tokens?.slack;
@@ -4229,12 +4529,13 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
4229
4529
  await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: errCode, medium: "slack" });
4230
4530
  return;
4231
4531
  }
4232
- const sent = await sendSlackChannelMessage(agentCodeName, openJson.channel.id, footeredBody);
4532
+ const sent = await postSlackChannelMessage(agentCodeName, openJson.channel.id, footeredBody);
4233
4533
  await reportDeliveryStatus(agentId, taskId, {
4234
- status: sent ? "ok" : "failed",
4534
+ status: sent.ok ? "ok" : "failed",
4235
4535
  medium: "slack",
4236
- error_code: sent ? null : "SLACK_SEND_FAILED"
4536
+ error_code: sent.ok ? null : "SLACK_SEND_FAILED"
4237
4537
  });
4538
+ if (sent.ok) await maybePostSlackThreadHint(agentCodeName, openJson.channel.id, sent.ts);
4238
4539
  } catch (err) {
4239
4540
  const isAbort = err.name === "AbortError";
4240
4541
  const errCode = isAbort ? "SLACK_OPEN_TIMEOUT" : "SLACK_EXCEPTION";
@@ -4262,6 +4563,7 @@ async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, bod
4262
4563
  return;
4263
4564
  }
4264
4565
  await reportDeliveryStatus(agentId, taskId, { status: "ok", medium: "telegram" });
4566
+ await maybeSendTelegramFollowUpHint(agentCodeName, botToken, resolved.telegram_chat_id);
4265
4567
  } catch (err) {
4266
4568
  log(`[delivery] Telegram DM exception for '${agentCodeName}': ${err.message}`);
4267
4569
  await reportDeliveryStatus(agentId, taskId, { status: "failed", error_code: "TELEGRAM_EXCEPTION", medium: "telegram" });