@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.
- package/dist/bin/agt.js +3 -3
- package/dist/{chunk-ZFTZDO5E.js → chunk-WNYQKTBP.js} +1 -1
- package/dist/chunk-WNYQKTBP.js.map +1 -0
- package/dist/lib/manager-worker.js +276 -32
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/direct-chat-channel.js +1 -0
- package/mcp/index.js +3 -6
- package/mcp/slack-channel.js +1 -0
- package/mcp/telegram-channel.js +137 -4
- package/package.json +1 -1
- package/dist/chunk-ZFTZDO5E.js.map +0 -1
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
resolveChannels,
|
|
18
18
|
resolveDmTarget,
|
|
19
19
|
wrapScheduledTaskPrompt
|
|
20
|
-
} from "../chunk-
|
|
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(
|
|
1007
|
+
function resolveBrewPath(execFileSync2) {
|
|
815
1008
|
try {
|
|
816
|
-
const out =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1070
|
+
execFileSync2("sudo", ["-u", "ec2-user", "-H", brewPath, "install", pkg], { timeout: 18e4, stdio: "pipe" });
|
|
878
1071
|
} else {
|
|
879
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1117
|
+
return execFileSync2("sudo", sudoArgs, { ...opts, stdio: "pipe" }).toString();
|
|
925
1118
|
}
|
|
926
|
-
return
|
|
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
|
-
|
|
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 =
|
|
1180
|
+
brewPath = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
|
|
988
1181
|
} catch {
|
|
989
1182
|
return;
|
|
990
1183
|
}
|
|
991
1184
|
try {
|
|
992
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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" });
|