@integrity-labs/agt-cli 0.27.8-test.9 → 0.27.9-test.11
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/assets/impersonate-statusline.sh +7 -2
- package/dist/bin/agt.js +798 -98
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-HT6EETEL.js → chunk-45I2X4M7.js} +361 -26
- package/dist/chunk-45I2X4M7.js.map +1 -0
- package/dist/{chunk-NFBRJGNU.js → chunk-N2EBL4PZ.js} +915 -239
- package/dist/chunk-N2EBL4PZ.js.map +1 -0
- package/dist/{chunk-F4NG4EXD.js → chunk-SJPAXXTW.js} +512 -80
- package/dist/chunk-SJPAXXTW.js.map +1 -0
- package/dist/{claude-pair-runtime-OBAJZDXK.js → claude-pair-runtime-OUGV74LY.js} +2 -2
- package/dist/lib/manager-worker.js +2058 -508
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/index.js +143 -67
- package/dist/mcp/slack-channel.js +1059 -297
- package/dist/mcp/teams-channel.js +16018 -0
- package/dist/mcp/telegram-channel.js +999 -140
- package/dist/{persistent-session-SBSOZG74.js → persistent-session-G3MR7JVI.js} +5 -3
- package/dist/responsiveness-probe-UHSBETBR.js +74 -0
- package/dist/responsiveness-probe-UHSBETBR.js.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-F4NG4EXD.js.map +0 -1
- package/dist/chunk-HT6EETEL.js.map +0 -1
- package/dist/chunk-NFBRJGNU.js.map +0 -1
- package/dist/responsiveness-probe-DU4IJ2RZ.js +0 -33
- package/dist/responsiveness-probe-DU4IJ2RZ.js.map +0 -1
- /package/dist/{claude-pair-runtime-OBAJZDXK.js.map → claude-pair-runtime-OUGV74LY.js.map} +0 -0
- /package/dist/{persistent-session-SBSOZG74.js.map → persistent-session-G3MR7JVI.js.map} +0 -0
|
@@ -6986,13 +6986,10 @@ __export(slack_block_kit_runtime_exports, {
|
|
|
6986
6986
|
DEFAULT_ASK_USER_LIMITS: () => DEFAULT_ASK_USER_LIMITS,
|
|
6987
6987
|
apiCall: () => apiCall,
|
|
6988
6988
|
buildAskUserBlocks: () => buildAskUserBlocks,
|
|
6989
|
-
buildRatingActionsBlock: () => buildRatingActionsBlock,
|
|
6990
6989
|
createPendingInteraction: () => createPendingInteraction,
|
|
6991
6990
|
decodeActionId: () => decodeActionId,
|
|
6992
|
-
defaultRatingLabels: () => defaultRatingLabels,
|
|
6993
6991
|
encodeActionId: () => encodeActionId,
|
|
6994
6992
|
generateOptionToken: () => generateOptionToken,
|
|
6995
|
-
ratingValuesForScale: () => ratingValuesForScale,
|
|
6996
6993
|
recordSlackDelivery: () => recordSlackDelivery,
|
|
6997
6994
|
resolveInteractive: () => resolveInteractive,
|
|
6998
6995
|
updatePendingInteractionMessageTs: () => updatePendingInteractionMessageTs,
|
|
@@ -7159,22 +7156,6 @@ function buildAskUserBlocks(opts) {
|
|
|
7159
7156
|
}
|
|
7160
7157
|
];
|
|
7161
7158
|
}
|
|
7162
|
-
function ratingValuesForScale(scale) {
|
|
7163
|
-
return scale === "1-5" ? [1, 2, 3, 4, 5] : [-1, 0, 1];
|
|
7164
|
-
}
|
|
7165
|
-
function defaultRatingLabels(scale) {
|
|
7166
|
-
return scale === "1-5" ? ["1", "2", "3", "4", "5"] : ["\u{1F44E}", "\u{1F914}", "\u{1F44D}"];
|
|
7167
|
-
}
|
|
7168
|
-
function buildRatingActionsBlock(opts) {
|
|
7169
|
-
return {
|
|
7170
|
-
type: "actions",
|
|
7171
|
-
elements: opts.options.map(({ label, token }) => ({
|
|
7172
|
-
type: "button",
|
|
7173
|
-
text: { type: "plain_text", text: label },
|
|
7174
|
-
action_id: encodeActionId(opts.callbackId, token)
|
|
7175
|
-
}))
|
|
7176
|
-
};
|
|
7177
|
-
}
|
|
7178
7159
|
async function resolveInteractive(cfg, input) {
|
|
7179
7160
|
const res = await apiCall(cfg, "POST", "/host/interactive/resolve", {
|
|
7180
7161
|
agent_id: cfg.agentId,
|
|
@@ -14237,23 +14218,95 @@ function extractAugmentedSlackLabel(evt) {
|
|
|
14237
14218
|
agentId: evt.metadata.event_payload?.["augmented_agent_id"]
|
|
14238
14219
|
};
|
|
14239
14220
|
}
|
|
14240
|
-
function
|
|
14221
|
+
function decideModeForward(evt, policy) {
|
|
14241
14222
|
if (policy.mode === "all") return { forward: true };
|
|
14223
|
+
if (policy.mode === "manager_only") {
|
|
14224
|
+
if (policy.principalSlackUserId && evt.user === policy.principalSlackUserId) {
|
|
14225
|
+
return { forward: true };
|
|
14226
|
+
}
|
|
14227
|
+
}
|
|
14228
|
+
if (policy.mode === "team_only") {
|
|
14229
|
+
if (policy.teamPrincipalSlackUserIds && policy.teamPrincipalSlackUserIds.length > 0 && typeof evt.user === "string" && policy.teamPrincipalSlackUserIds.includes(evt.user)) {
|
|
14230
|
+
return { forward: true };
|
|
14231
|
+
}
|
|
14232
|
+
}
|
|
14242
14233
|
const label = extractAugmentedSlackLabel(evt);
|
|
14243
14234
|
if (!label) return { forward: false, reason: "sender_policy" };
|
|
14244
|
-
if (policy.mode === "team_agents_only") {
|
|
14235
|
+
if (policy.mode === "team_agents_only" || policy.mode === "manager_only" || policy.mode === "team_only") {
|
|
14245
14236
|
if (!policy.teamId || label.teamId !== policy.teamId) {
|
|
14246
14237
|
return { forward: false, reason: "sender_policy" };
|
|
14247
14238
|
}
|
|
14248
14239
|
}
|
|
14249
14240
|
return { forward: true };
|
|
14250
14241
|
}
|
|
14242
|
+
function decideSenderPolicyForward(evt, policy) {
|
|
14243
|
+
if (policy.internalOnly) {
|
|
14244
|
+
if (!policy.homeTeamId) return { forward: false, reason: "sender_policy" };
|
|
14245
|
+
if (evt.team !== policy.homeTeamId) return { forward: false, reason: "sender_policy" };
|
|
14246
|
+
}
|
|
14247
|
+
return decideModeForward(evt, policy);
|
|
14248
|
+
}
|
|
14249
|
+
|
|
14250
|
+
// src/sender-policy-decline.ts
|
|
14251
|
+
function classifySlackPolicyBlock(evt, policy) {
|
|
14252
|
+
if (policy.internalOnly && (!policy.homeTeamId || evt.team !== policy.homeTeamId)) {
|
|
14253
|
+
return "internal_only";
|
|
14254
|
+
}
|
|
14255
|
+
switch (policy.mode) {
|
|
14256
|
+
case "manager_only":
|
|
14257
|
+
return "mode_manager_only";
|
|
14258
|
+
case "team_only":
|
|
14259
|
+
return "mode_team_only";
|
|
14260
|
+
case "team_agents_only":
|
|
14261
|
+
return "mode_team_agents_only";
|
|
14262
|
+
case "agents_only":
|
|
14263
|
+
return "mode_agents_only";
|
|
14264
|
+
case "all":
|
|
14265
|
+
return "internal_only";
|
|
14266
|
+
}
|
|
14267
|
+
}
|
|
14268
|
+
function politeDeclineCopy(reason) {
|
|
14269
|
+
switch (reason) {
|
|
14270
|
+
case "internal_only":
|
|
14271
|
+
return "Sorry, I can only respond to people inside our organisation. This conversation appears to be from outside.";
|
|
14272
|
+
case "mode_manager_only":
|
|
14273
|
+
return "Sorry, I only respond to my manager in this channel.";
|
|
14274
|
+
case "mode_team_only":
|
|
14275
|
+
return "Sorry, this agent only replies to its team members.";
|
|
14276
|
+
case "mode_team_agents_only":
|
|
14277
|
+
return "Sorry, I only respond to agents on my team in this channel.";
|
|
14278
|
+
case "mode_agents_only":
|
|
14279
|
+
return "Sorry, I only respond to other agents in this channel.";
|
|
14280
|
+
}
|
|
14281
|
+
}
|
|
14282
|
+
function decideDeclineReply(input) {
|
|
14283
|
+
const last = input.cache.get(input.key);
|
|
14284
|
+
if (last !== void 0) {
|
|
14285
|
+
const elapsed = input.now - last;
|
|
14286
|
+
if (elapsed < input.cooldownMs) {
|
|
14287
|
+
return { reply: false, remainingMs: input.cooldownMs - elapsed };
|
|
14288
|
+
}
|
|
14289
|
+
}
|
|
14290
|
+
input.cache.set(input.key, input.now);
|
|
14291
|
+
return { reply: true };
|
|
14292
|
+
}
|
|
14293
|
+
function declineCacheKey(channelId, senderId, reason) {
|
|
14294
|
+
return `${channelId}|${senderId}|${reason}`;
|
|
14295
|
+
}
|
|
14296
|
+
function readDeclineCooldownMs(envVarName, defaultSeconds = 1800) {
|
|
14297
|
+
const raw = process.env[envVarName];
|
|
14298
|
+
if (!raw) return defaultSeconds * 1e3;
|
|
14299
|
+
const parsed = Number(raw);
|
|
14300
|
+
if (!Number.isFinite(parsed) || parsed < 0) return defaultSeconds * 1e3;
|
|
14301
|
+
return parsed * 1e3;
|
|
14302
|
+
}
|
|
14251
14303
|
|
|
14252
14304
|
// src/ack-reaction.ts
|
|
14253
14305
|
import { readdirSync, readFileSync } from "fs";
|
|
14254
14306
|
import { join } from "path";
|
|
14255
14307
|
var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
14256
14308
|
var ACK_STARTUP_GRACE_MS = 6e4;
|
|
14309
|
+
var ACK_PANE_FRESH_THRESHOLD_MS = 6e4;
|
|
14257
14310
|
function decideAckReaction(i) {
|
|
14258
14311
|
if (!i.hasTarget) return "none";
|
|
14259
14312
|
if (!i.integrationReady) return "undeliverable";
|
|
@@ -14261,6 +14314,11 @@ function decideAckReaction(i) {
|
|
|
14261
14314
|
if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
|
|
14262
14315
|
const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
|
|
14263
14316
|
if (i.oldestPendingAgeMs != null && i.oldestPendingAgeMs > threshold) {
|
|
14317
|
+
const paneFreshThreshold = i.paneFreshThresholdMs ?? ACK_PANE_FRESH_THRESHOLD_MS;
|
|
14318
|
+
const paneIsFresh = i.paneLogFreshAgeMs != null && i.paneLogFreshAgeMs <= paneFreshThreshold;
|
|
14319
|
+
if (paneIsFresh && i.tmux === "alive" && i.claude === "alive") {
|
|
14320
|
+
return "ack";
|
|
14321
|
+
}
|
|
14264
14322
|
return "undeliverable";
|
|
14265
14323
|
}
|
|
14266
14324
|
return "ack";
|
|
@@ -14268,6 +14326,39 @@ function decideAckReaction(i) {
|
|
|
14268
14326
|
function decideRecoveryHeal(i) {
|
|
14269
14327
|
return i.wasUndeliverable && i.hasTarget ? "heal" : "none";
|
|
14270
14328
|
}
|
|
14329
|
+
var SLACK_UNDELIVERABLE_REACTION = "hourglass_flowing_sand";
|
|
14330
|
+
var SLACK_UNDELIVERABLE_REACTION_LEGACY = "x";
|
|
14331
|
+
var UNDELIVERABLE_NOTICE_THROTTLE_MS = 5 * 60 * 1e3;
|
|
14332
|
+
function shouldPostUndeliverableNotice(lastNoticeAtMs, nowMs, throttleMs = UNDELIVERABLE_NOTICE_THROTTLE_MS) {
|
|
14333
|
+
return lastNoticeAtMs == null || nowMs - lastNoticeAtMs >= throttleMs;
|
|
14334
|
+
}
|
|
14335
|
+
function undeliverableNoticeText() {
|
|
14336
|
+
return "\u23F3 I can't get to this right now. Please resend in a few minutes \u2014 I may not see this one.";
|
|
14337
|
+
}
|
|
14338
|
+
var GIVE_UP_SIGNAL_FILENAME = "watchdog-give-up.json";
|
|
14339
|
+
var GIVE_UP_SIGNAL_MAX_AGE_MS = 30 * 60 * 1e3;
|
|
14340
|
+
function readGiveUpSignalAtMs(path, now = Date.now()) {
|
|
14341
|
+
if (!path) return null;
|
|
14342
|
+
try {
|
|
14343
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
14344
|
+
if (typeof raw.gave_up_at !== "string") return null;
|
|
14345
|
+
const t = Date.parse(raw.gave_up_at);
|
|
14346
|
+
if (!Number.isFinite(t) || t > now) return null;
|
|
14347
|
+
return t;
|
|
14348
|
+
} catch {
|
|
14349
|
+
return null;
|
|
14350
|
+
}
|
|
14351
|
+
}
|
|
14352
|
+
function decideGiveUpNotice(input) {
|
|
14353
|
+
const maxAge = input.maxAgeMs ?? GIVE_UP_SIGNAL_MAX_AGE_MS;
|
|
14354
|
+
if (input.signalAtMs == null) return false;
|
|
14355
|
+
if (input.nowMs - input.signalAtMs > maxAge) return false;
|
|
14356
|
+
if (input.lastHandledAtMs != null && input.signalAtMs <= input.lastHandledAtMs) return false;
|
|
14357
|
+
return true;
|
|
14358
|
+
}
|
|
14359
|
+
function giveUpNoticeText() {
|
|
14360
|
+
return "\u26A0\uFE0F I couldn't read your last message \u2014 please resend it.";
|
|
14361
|
+
}
|
|
14271
14362
|
function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
|
|
14272
14363
|
if (!dir) return null;
|
|
14273
14364
|
let names;
|
|
@@ -14295,6 +14386,21 @@ function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
|
|
|
14295
14386
|
}
|
|
14296
14387
|
return oldest;
|
|
14297
14388
|
}
|
|
14389
|
+
function isPendingMarkerStale(receivedAt, nowMs, thresholdMs) {
|
|
14390
|
+
if (!receivedAt) return true;
|
|
14391
|
+
const t = Date.parse(receivedAt);
|
|
14392
|
+
if (!Number.isFinite(t)) return true;
|
|
14393
|
+
if (t > nowMs) return true;
|
|
14394
|
+
return nowMs - t > thresholdMs;
|
|
14395
|
+
}
|
|
14396
|
+
function channelOrphanMarkerMs() {
|
|
14397
|
+
const raw = parseInt(process.env.AGT_CHANNEL_ORPHAN_MARKER_MS ?? "", 10);
|
|
14398
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 30 * 6e4;
|
|
14399
|
+
}
|
|
14400
|
+
var ORPHAN_SWEEP_INTERVAL_MS = 30 * 60 * 1e3;
|
|
14401
|
+
function orphanSweepIntervalMs() {
|
|
14402
|
+
return Math.max(6e4, Math.min(ORPHAN_SWEEP_INTERVAL_MS, channelOrphanMarkerMs()));
|
|
14403
|
+
}
|
|
14298
14404
|
|
|
14299
14405
|
// src/session-probe-runtime.ts
|
|
14300
14406
|
import { execFileSync } from "child_process";
|
|
@@ -14348,6 +14454,233 @@ function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = D
|
|
|
14348
14454
|
return value;
|
|
14349
14455
|
}
|
|
14350
14456
|
|
|
14457
|
+
// src/pane-tail.ts
|
|
14458
|
+
import { execFile } from "child_process";
|
|
14459
|
+
import { promisify } from "util";
|
|
14460
|
+
import { open, stat } from "fs/promises";
|
|
14461
|
+
|
|
14462
|
+
// src/channel-attachments.ts
|
|
14463
|
+
import { homedir } from "os";
|
|
14464
|
+
import { join as join2, resolve, sep } from "path";
|
|
14465
|
+
function resolveChannelInboundDir(codeName, channelSlug) {
|
|
14466
|
+
const base = join2(homedir(), ".augmented");
|
|
14467
|
+
const allowedSegment = /^[A-Za-z0-9_-]+$/;
|
|
14468
|
+
if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
|
|
14469
|
+
throw new Error(
|
|
14470
|
+
`Refusing to resolve inbound dir \u2014 invalid codeName/channelSlug (got ${JSON.stringify({ codeName, channelSlug })})`
|
|
14471
|
+
);
|
|
14472
|
+
}
|
|
14473
|
+
const candidate = resolve(base, codeName, channelSlug);
|
|
14474
|
+
if (!isPathInside(candidate, base)) {
|
|
14475
|
+
throw new Error(`Refusing inbound dir outside ${base} (got ${candidate})`);
|
|
14476
|
+
}
|
|
14477
|
+
return candidate;
|
|
14478
|
+
}
|
|
14479
|
+
function classifyMimetype(mimetype) {
|
|
14480
|
+
if (typeof mimetype === "string" && mimetype.startsWith("image/")) return "image";
|
|
14481
|
+
return "attachment";
|
|
14482
|
+
}
|
|
14483
|
+
function buildSafeInboundPath(root, fileId, mimetype) {
|
|
14484
|
+
const safeId = fileId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
14485
|
+
if (!safeId) throw new Error("Refusing to build inbound path for empty/invalid file id");
|
|
14486
|
+
const ext = extensionForMimetype(mimetype);
|
|
14487
|
+
const candidate = resolve(root, `${safeId}${ext}`);
|
|
14488
|
+
if (!isPathInside(candidate, root)) {
|
|
14489
|
+
throw new Error(`Refusing to build inbound path outside agent dir (got ${candidate})`);
|
|
14490
|
+
}
|
|
14491
|
+
return candidate;
|
|
14492
|
+
}
|
|
14493
|
+
function extensionForMimetype(mimetype) {
|
|
14494
|
+
if (!mimetype) return ".bin";
|
|
14495
|
+
switch (mimetype) {
|
|
14496
|
+
// Images
|
|
14497
|
+
case "image/png":
|
|
14498
|
+
return ".png";
|
|
14499
|
+
case "image/jpeg":
|
|
14500
|
+
case "image/jpg":
|
|
14501
|
+
return ".jpg";
|
|
14502
|
+
case "image/gif":
|
|
14503
|
+
return ".gif";
|
|
14504
|
+
case "image/webp":
|
|
14505
|
+
return ".webp";
|
|
14506
|
+
case "image/svg+xml":
|
|
14507
|
+
return ".svg";
|
|
14508
|
+
// Docs
|
|
14509
|
+
case "application/pdf":
|
|
14510
|
+
return ".pdf";
|
|
14511
|
+
case "text/plain":
|
|
14512
|
+
return ".txt";
|
|
14513
|
+
case "text/csv":
|
|
14514
|
+
return ".csv";
|
|
14515
|
+
case "application/json":
|
|
14516
|
+
return ".json";
|
|
14517
|
+
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
14518
|
+
return ".docx";
|
|
14519
|
+
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
|
14520
|
+
return ".xlsx";
|
|
14521
|
+
// Audio (Telegram voice notes are typically audio/ogg; regular
|
|
14522
|
+
// audio messages are audio/mpeg)
|
|
14523
|
+
case "audio/ogg":
|
|
14524
|
+
return ".ogg";
|
|
14525
|
+
case "audio/mpeg":
|
|
14526
|
+
return ".mp3";
|
|
14527
|
+
case "audio/mp4":
|
|
14528
|
+
return ".m4a";
|
|
14529
|
+
// Video
|
|
14530
|
+
case "video/mp4":
|
|
14531
|
+
return ".mp4";
|
|
14532
|
+
case "video/quicktime":
|
|
14533
|
+
return ".mov";
|
|
14534
|
+
default:
|
|
14535
|
+
return ".bin";
|
|
14536
|
+
}
|
|
14537
|
+
}
|
|
14538
|
+
function isPathInside(target, root) {
|
|
14539
|
+
const normalizedRoot = resolve(root) + sep;
|
|
14540
|
+
const normalizedTarget = resolve(target);
|
|
14541
|
+
return normalizedTarget === resolve(root) || normalizedTarget.startsWith(normalizedRoot);
|
|
14542
|
+
}
|
|
14543
|
+
function redactAugmentedPaths(msg) {
|
|
14544
|
+
const homePrefix = homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14545
|
+
return msg.replaceAll(
|
|
14546
|
+
new RegExp(`${homePrefix}[\\\\/]\\.augmented(?:[\\\\/][^\\s'"\`]*)*`, "g"),
|
|
14547
|
+
"<augmented-path>"
|
|
14548
|
+
);
|
|
14549
|
+
}
|
|
14550
|
+
|
|
14551
|
+
// src/pane-tail.ts
|
|
14552
|
+
var execFileAsync = promisify(execFile);
|
|
14553
|
+
function evaluateDebugGate(opts) {
|
|
14554
|
+
if (!opts.channelId || !opts.channelId.startsWith("D")) {
|
|
14555
|
+
return { ok: false, reason: "not-dm" };
|
|
14556
|
+
}
|
|
14557
|
+
if (opts.allowedUsers.size === 0) {
|
|
14558
|
+
return { ok: false, reason: "allowlist-empty" };
|
|
14559
|
+
}
|
|
14560
|
+
if (!opts.userId || !opts.allowedUsers.has(opts.userId)) {
|
|
14561
|
+
return { ok: false, reason: "not-allowlisted" };
|
|
14562
|
+
}
|
|
14563
|
+
return { ok: true };
|
|
14564
|
+
}
|
|
14565
|
+
var PANE_LOG_TAIL_BYTES = 64 * 1024;
|
|
14566
|
+
var TMUX_CAPTURE_TIMEOUT_MS = 2e3;
|
|
14567
|
+
async function capturePaneSnapshot(opts) {
|
|
14568
|
+
const scrollback = opts.scrollbackLines ?? 200;
|
|
14569
|
+
try {
|
|
14570
|
+
const { stdout } = await execFileAsync(
|
|
14571
|
+
"tmux",
|
|
14572
|
+
["capture-pane", "-p", "-t", agentTmuxSessionName(opts.codeName), "-S", `-${scrollback}`],
|
|
14573
|
+
{ timeout: TMUX_CAPTURE_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }
|
|
14574
|
+
);
|
|
14575
|
+
return { source: "tmux", text: stdout };
|
|
14576
|
+
} catch {
|
|
14577
|
+
}
|
|
14578
|
+
if (!opts.agentDir) return null;
|
|
14579
|
+
const paneLogPath = `${opts.agentDir}/pane.log`;
|
|
14580
|
+
try {
|
|
14581
|
+
const tail = await readFileTail(paneLogPath, PANE_LOG_TAIL_BYTES);
|
|
14582
|
+
if (tail === null) return null;
|
|
14583
|
+
return { source: "pane-log", text: stripAnsi(tail) };
|
|
14584
|
+
} catch {
|
|
14585
|
+
return null;
|
|
14586
|
+
}
|
|
14587
|
+
}
|
|
14588
|
+
async function readFileTail(path, maxBytes) {
|
|
14589
|
+
const st = await stat(path);
|
|
14590
|
+
if (!st.isFile()) return null;
|
|
14591
|
+
const start = Math.max(0, st.size - maxBytes);
|
|
14592
|
+
const length = st.size - start;
|
|
14593
|
+
if (length === 0) return "";
|
|
14594
|
+
const handle = await open(path, "r");
|
|
14595
|
+
try {
|
|
14596
|
+
const buf = Buffer.alloc(length);
|
|
14597
|
+
await handle.read(buf, 0, length, start);
|
|
14598
|
+
let text = buf.toString("utf8");
|
|
14599
|
+
if (start > 0) {
|
|
14600
|
+
const nl = text.indexOf("\n");
|
|
14601
|
+
if (nl !== -1) text = text.slice(nl + 1);
|
|
14602
|
+
}
|
|
14603
|
+
return text;
|
|
14604
|
+
} finally {
|
|
14605
|
+
await handle.close();
|
|
14606
|
+
}
|
|
14607
|
+
}
|
|
14608
|
+
function stripAnsi(text) {
|
|
14609
|
+
return text.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;?<=>!]*[ -\/]*[@-~]/g, "").replace(/\x1b./g, "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
|
|
14610
|
+
}
|
|
14611
|
+
var SECRET_PATTERNS = [
|
|
14612
|
+
// Augmented host API keys
|
|
14613
|
+
{ re: /tlk_[A-Za-z0-9]{8,}/g, label: "agt-api-key" },
|
|
14614
|
+
// Slack tokens: bot/app/user/refresh/config
|
|
14615
|
+
{ re: /x(?:ox[abprs]|app)-[A-Za-z0-9-]{8,}/g, label: "slack-token" },
|
|
14616
|
+
// JWTs (three base64url segments, first one always starts with eyJ)
|
|
14617
|
+
{ re: /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, label: "jwt" },
|
|
14618
|
+
// OpenAI / Anthropic-style keys (sk-..., sk-ant-...)
|
|
14619
|
+
{ re: /sk-[A-Za-z0-9_-]{16,}/g, label: "sk-key" },
|
|
14620
|
+
// AWS access key IDs
|
|
14621
|
+
{ re: /(?:AKIA|ASIA)[0-9A-Z]{16}/g, label: "aws-key" },
|
|
14622
|
+
// GitHub tokens
|
|
14623
|
+
{ re: /gh[pousr]_[A-Za-z0-9]{20,}/g, label: "github-token" }
|
|
14624
|
+
];
|
|
14625
|
+
function redactPaneText(text) {
|
|
14626
|
+
let out = redactAugmentedPaths(text);
|
|
14627
|
+
for (const { re, label } of SECRET_PATTERNS) {
|
|
14628
|
+
out = out.replace(re, `<redacted:${label}>`);
|
|
14629
|
+
}
|
|
14630
|
+
return out;
|
|
14631
|
+
}
|
|
14632
|
+
var DEFAULT_MAX_CHARS = 3900;
|
|
14633
|
+
var REFLOW_COLS = 80;
|
|
14634
|
+
function formatPaneSnapshot(opts) {
|
|
14635
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
14636
|
+
const commandName = opts.commandName ?? "/debug";
|
|
14637
|
+
const header = opts.status.kind === "live" ? `\u{1F50E} Live pane tail for \`${opts.codeName}\` \u2014 captured ${opts.capturedAtLabel} \xB7 updates ~3s \xB7 expires in ${formatCountdown(opts.status.secondsRemaining)}` : `\u{1F50E} Pane tail for \`${opts.codeName}\` \u2014 captured ${opts.capturedAtLabel} \xB7 *expired* \u2014 run \`${commandName}\` to restart`;
|
|
14638
|
+
const fenceOverhead = header.length + "\n```\n".length + "\n```".length + 16;
|
|
14639
|
+
const contentBudget = Math.max(0, maxChars - fenceOverhead);
|
|
14640
|
+
const lines = reflowLines(sanitizeForCodeBlock(opts.text), REFLOW_COLS);
|
|
14641
|
+
const kept = [];
|
|
14642
|
+
let used = 0;
|
|
14643
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
14644
|
+
const cost = lines[i].length + 1;
|
|
14645
|
+
if (used + cost > contentBudget) break;
|
|
14646
|
+
kept.unshift(lines[i]);
|
|
14647
|
+
used += cost;
|
|
14648
|
+
}
|
|
14649
|
+
const truncated = kept.length < lines.length;
|
|
14650
|
+
const body = kept.join("\n").trimEnd();
|
|
14651
|
+
const block = body.length > 0 ? `\`\`\`
|
|
14652
|
+
${truncated ? "\u2026\n" : ""}${body}
|
|
14653
|
+
\`\`\`` : "_(pane is empty)_";
|
|
14654
|
+
return `${header}
|
|
14655
|
+
${block}`;
|
|
14656
|
+
}
|
|
14657
|
+
function formatCountdown(totalSeconds) {
|
|
14658
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
14659
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
14660
|
+
}
|
|
14661
|
+
function sanitizeForCodeBlock(text) {
|
|
14662
|
+
return text.replace(/```/g, "`\u200B`\u200B`");
|
|
14663
|
+
}
|
|
14664
|
+
function reflowLines(text, cols) {
|
|
14665
|
+
const out = [];
|
|
14666
|
+
for (const rawLine of text.split("\n")) {
|
|
14667
|
+
const line = rawLine.replace(/\s+$/, "");
|
|
14668
|
+
if (line.length <= cols) {
|
|
14669
|
+
out.push(line);
|
|
14670
|
+
continue;
|
|
14671
|
+
}
|
|
14672
|
+
for (let i = 0; i < line.length; i += cols) {
|
|
14673
|
+
out.push(line.slice(i, i + cols));
|
|
14674
|
+
}
|
|
14675
|
+
}
|
|
14676
|
+
const collapsed = [];
|
|
14677
|
+
for (const line of out) {
|
|
14678
|
+
if (line === "" && collapsed[collapsed.length - 1] === "") continue;
|
|
14679
|
+
collapsed.push(line);
|
|
14680
|
+
}
|
|
14681
|
+
return collapsed;
|
|
14682
|
+
}
|
|
14683
|
+
|
|
14351
14684
|
// src/slack-loop-throttle.ts
|
|
14352
14685
|
var DEFAULT_THROTTLE = {
|
|
14353
14686
|
threshold: 3,
|
|
@@ -14555,6 +14888,69 @@ var SLACK_EGRESS_TOOLS = /* @__PURE__ */ new Set([
|
|
|
14555
14888
|
"slack.upload_file"
|
|
14556
14889
|
]);
|
|
14557
14890
|
|
|
14891
|
+
// src/slack-pending-inbound-cleanup.ts
|
|
14892
|
+
import { existsSync, readdirSync as readdirSync2, statSync, unlinkSync } from "fs";
|
|
14893
|
+
import { join as join3 } from "path";
|
|
14894
|
+
function sanitizeMarkerSegment(value) {
|
|
14895
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
14896
|
+
}
|
|
14897
|
+
var defaultClearMarkerFile = (fullPath) => {
|
|
14898
|
+
try {
|
|
14899
|
+
if (existsSync(fullPath)) unlinkSync(fullPath);
|
|
14900
|
+
} catch {
|
|
14901
|
+
}
|
|
14902
|
+
};
|
|
14903
|
+
function clearAllSlackPendingMarkersForThread(dir, channel, threadTs, clear = defaultClearMarkerFile) {
|
|
14904
|
+
if (!dir) return 0;
|
|
14905
|
+
const prefix = `${sanitizeMarkerSegment(channel)}__${sanitizeMarkerSegment(threadTs)}__`;
|
|
14906
|
+
let cleared = 0;
|
|
14907
|
+
try {
|
|
14908
|
+
for (const f of readdirSync2(dir)) {
|
|
14909
|
+
if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
|
|
14910
|
+
clear(join3(dir, f));
|
|
14911
|
+
cleared += 1;
|
|
14912
|
+
}
|
|
14913
|
+
} catch {
|
|
14914
|
+
}
|
|
14915
|
+
return cleared;
|
|
14916
|
+
}
|
|
14917
|
+
function clearSlackPendingMarkerByMessageTs(dir, channel, messageTs, clear = defaultClearMarkerFile) {
|
|
14918
|
+
if (!dir) return 0;
|
|
14919
|
+
const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
|
|
14920
|
+
const messageSuffix = `__${sanitizeMarkerSegment(messageTs)}.json`;
|
|
14921
|
+
let cleared = 0;
|
|
14922
|
+
try {
|
|
14923
|
+
for (const f of readdirSync2(dir)) {
|
|
14924
|
+
if (!f.startsWith(channelPrefix) || !f.endsWith(messageSuffix)) continue;
|
|
14925
|
+
clear(join3(dir, f));
|
|
14926
|
+
cleared += 1;
|
|
14927
|
+
}
|
|
14928
|
+
} catch {
|
|
14929
|
+
}
|
|
14930
|
+
return cleared;
|
|
14931
|
+
}
|
|
14932
|
+
function clearOldestSlackPendingMarkerInChannel(dir, channel, clear = defaultClearMarkerFile) {
|
|
14933
|
+
if (!dir) return null;
|
|
14934
|
+
const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
|
|
14935
|
+
try {
|
|
14936
|
+
const entries = readdirSync2(dir).filter((f) => f.startsWith(channelPrefix) && f.endsWith(".json")).map((f) => {
|
|
14937
|
+
const full = join3(dir, f);
|
|
14938
|
+
let mtime = 0;
|
|
14939
|
+
try {
|
|
14940
|
+
mtime = statSync(full).mtimeMs;
|
|
14941
|
+
} catch {
|
|
14942
|
+
}
|
|
14943
|
+
return { name: f, full, mtime };
|
|
14944
|
+
}).filter((e) => e.mtime > 0).sort((a, b) => a.mtime - b.mtime);
|
|
14945
|
+
const oldest = entries[0];
|
|
14946
|
+
if (!oldest) return null;
|
|
14947
|
+
clear(oldest.full);
|
|
14948
|
+
return oldest.name;
|
|
14949
|
+
} catch {
|
|
14950
|
+
return null;
|
|
14951
|
+
}
|
|
14952
|
+
}
|
|
14953
|
+
|
|
14558
14954
|
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
14559
14955
|
import process2 from "process";
|
|
14560
14956
|
|
|
@@ -14651,17 +15047,17 @@ var StdioServerTransport = class {
|
|
|
14651
15047
|
import {
|
|
14652
15048
|
chmodSync,
|
|
14653
15049
|
createWriteStream,
|
|
14654
|
-
existsSync as
|
|
15050
|
+
existsSync as existsSync3,
|
|
14655
15051
|
mkdirSync as mkdirSync3,
|
|
14656
15052
|
readFileSync as readFileSync4,
|
|
14657
|
-
readdirSync as
|
|
15053
|
+
readdirSync as readdirSync3,
|
|
14658
15054
|
renameSync as renameSync2,
|
|
14659
|
-
statSync,
|
|
14660
|
-
unlinkSync as
|
|
15055
|
+
statSync as statSync2,
|
|
15056
|
+
unlinkSync as unlinkSync3,
|
|
14661
15057
|
watch,
|
|
14662
15058
|
writeFileSync as writeFileSync3
|
|
14663
15059
|
} from "fs";
|
|
14664
|
-
import { basename, join as
|
|
15060
|
+
import { basename, join as join5, resolve as resolve2 } from "path";
|
|
14665
15061
|
import { homedir as homedir2 } from "os";
|
|
14666
15062
|
import { createHash, randomUUID } from "crypto";
|
|
14667
15063
|
|
|
@@ -14790,88 +15186,6 @@ async function runOrRetry(fn, opts) {
|
|
|
14790
15186
|
}
|
|
14791
15187
|
}
|
|
14792
15188
|
|
|
14793
|
-
// src/channel-attachments.ts
|
|
14794
|
-
import { homedir } from "os";
|
|
14795
|
-
import { join as join2, resolve, sep } from "path";
|
|
14796
|
-
function resolveChannelInboundDir(codeName, channelSlug) {
|
|
14797
|
-
const base = join2(homedir(), ".augmented");
|
|
14798
|
-
const allowedSegment = /^[A-Za-z0-9_-]+$/;
|
|
14799
|
-
if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
|
|
14800
|
-
throw new Error(
|
|
14801
|
-
`Refusing to resolve inbound dir \u2014 invalid codeName/channelSlug (got ${JSON.stringify({ codeName, channelSlug })})`
|
|
14802
|
-
);
|
|
14803
|
-
}
|
|
14804
|
-
const candidate = resolve(base, codeName, channelSlug);
|
|
14805
|
-
if (!isPathInside(candidate, base)) {
|
|
14806
|
-
throw new Error(`Refusing inbound dir outside ${base} (got ${candidate})`);
|
|
14807
|
-
}
|
|
14808
|
-
return candidate;
|
|
14809
|
-
}
|
|
14810
|
-
function classifyMimetype(mimetype) {
|
|
14811
|
-
if (typeof mimetype === "string" && mimetype.startsWith("image/")) return "image";
|
|
14812
|
-
return "attachment";
|
|
14813
|
-
}
|
|
14814
|
-
function buildSafeInboundPath(root, fileId, mimetype) {
|
|
14815
|
-
const safeId = fileId.replace(/[^A-Za-z0-9_-]/g, "");
|
|
14816
|
-
if (!safeId) throw new Error("Refusing to build inbound path for empty/invalid file id");
|
|
14817
|
-
const ext = extensionForMimetype(mimetype);
|
|
14818
|
-
const candidate = resolve(root, `${safeId}${ext}`);
|
|
14819
|
-
if (!isPathInside(candidate, root)) {
|
|
14820
|
-
throw new Error(`Refusing to build inbound path outside agent dir (got ${candidate})`);
|
|
14821
|
-
}
|
|
14822
|
-
return candidate;
|
|
14823
|
-
}
|
|
14824
|
-
function extensionForMimetype(mimetype) {
|
|
14825
|
-
if (!mimetype) return ".bin";
|
|
14826
|
-
switch (mimetype) {
|
|
14827
|
-
// Images
|
|
14828
|
-
case "image/png":
|
|
14829
|
-
return ".png";
|
|
14830
|
-
case "image/jpeg":
|
|
14831
|
-
case "image/jpg":
|
|
14832
|
-
return ".jpg";
|
|
14833
|
-
case "image/gif":
|
|
14834
|
-
return ".gif";
|
|
14835
|
-
case "image/webp":
|
|
14836
|
-
return ".webp";
|
|
14837
|
-
case "image/svg+xml":
|
|
14838
|
-
return ".svg";
|
|
14839
|
-
// Docs
|
|
14840
|
-
case "application/pdf":
|
|
14841
|
-
return ".pdf";
|
|
14842
|
-
case "text/plain":
|
|
14843
|
-
return ".txt";
|
|
14844
|
-
case "text/csv":
|
|
14845
|
-
return ".csv";
|
|
14846
|
-
case "application/json":
|
|
14847
|
-
return ".json";
|
|
14848
|
-
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
14849
|
-
return ".docx";
|
|
14850
|
-
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
|
14851
|
-
return ".xlsx";
|
|
14852
|
-
// Audio (Telegram voice notes are typically audio/ogg; regular
|
|
14853
|
-
// audio messages are audio/mpeg)
|
|
14854
|
-
case "audio/ogg":
|
|
14855
|
-
return ".ogg";
|
|
14856
|
-
case "audio/mpeg":
|
|
14857
|
-
return ".mp3";
|
|
14858
|
-
case "audio/mp4":
|
|
14859
|
-
return ".m4a";
|
|
14860
|
-
// Video
|
|
14861
|
-
case "video/mp4":
|
|
14862
|
-
return ".mp4";
|
|
14863
|
-
case "video/quicktime":
|
|
14864
|
-
return ".mov";
|
|
14865
|
-
default:
|
|
14866
|
-
return ".bin";
|
|
14867
|
-
}
|
|
14868
|
-
}
|
|
14869
|
-
function isPathInside(target, root) {
|
|
14870
|
-
const normalizedRoot = resolve(root) + sep;
|
|
14871
|
-
const normalizedTarget = resolve(target);
|
|
14872
|
-
return normalizedTarget === resolve(root) || normalizedTarget.startsWith(normalizedRoot);
|
|
14873
|
-
}
|
|
14874
|
-
|
|
14875
15189
|
// src/slack-inbound-files.ts
|
|
14876
15190
|
function classifySlackFile(file) {
|
|
14877
15191
|
if (!file || typeof file.id !== "string" || !file.id) return null;
|
|
@@ -15438,14 +15752,14 @@ function createSlackBotUserIdClient(args) {
|
|
|
15438
15752
|
|
|
15439
15753
|
// src/mcp-spawn-lock.ts
|
|
15440
15754
|
import {
|
|
15441
|
-
existsSync,
|
|
15755
|
+
existsSync as existsSync2,
|
|
15442
15756
|
mkdirSync as mkdirSync2,
|
|
15443
15757
|
readFileSync as readFileSync3,
|
|
15444
15758
|
renameSync,
|
|
15445
|
-
unlinkSync,
|
|
15759
|
+
unlinkSync as unlinkSync2,
|
|
15446
15760
|
writeFileSync as writeFileSync2
|
|
15447
15761
|
} from "fs";
|
|
15448
|
-
import { join as
|
|
15762
|
+
import { join as join4 } from "path";
|
|
15449
15763
|
function defaultIsPidAlive(pid) {
|
|
15450
15764
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
15451
15765
|
try {
|
|
@@ -15463,7 +15777,7 @@ function acquireMcpSpawnLock(args) {
|
|
|
15463
15777
|
const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
|
|
15464
15778
|
const selfPid = options.selfPid ?? process.pid;
|
|
15465
15779
|
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
15466
|
-
const path =
|
|
15780
|
+
const path = join4(agentDir, basename2);
|
|
15467
15781
|
const existing = readLockHolder(path);
|
|
15468
15782
|
if (existing) {
|
|
15469
15783
|
if (existing.pid === selfPid) {
|
|
@@ -15487,12 +15801,12 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
|
|
|
15487
15801
|
if (!existing) return;
|
|
15488
15802
|
if (existing.pid !== selfPid) return;
|
|
15489
15803
|
try {
|
|
15490
|
-
|
|
15804
|
+
unlinkSync2(lockPath);
|
|
15491
15805
|
} catch {
|
|
15492
15806
|
}
|
|
15493
15807
|
}
|
|
15494
15808
|
function readLockHolder(path) {
|
|
15495
|
-
if (!
|
|
15809
|
+
if (!existsSync2(path)) return null;
|
|
15496
15810
|
try {
|
|
15497
15811
|
const raw = readFileSync3(path, "utf8");
|
|
15498
15812
|
const parsed = JSON.parse(raw);
|
|
@@ -15509,22 +15823,117 @@ function readLockHolder(path) {
|
|
|
15509
15823
|
var BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
|
|
15510
15824
|
var APP_TOKEN = process.env.SLACK_APP_TOKEN;
|
|
15511
15825
|
var AGENT_CODE_NAME = process.env.AGT_AGENT_CODE_NAME ?? null;
|
|
15826
|
+
var SLACK_COMMAND_MAX_LENGTH = 32;
|
|
15827
|
+
function agentSlashCommand(base) {
|
|
15828
|
+
const codeName = AGENT_CODE_NAME?.trim().toLowerCase();
|
|
15829
|
+
if (!codeName || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(codeName)) return base;
|
|
15830
|
+
const suffixed = `${base}-${codeName}`;
|
|
15831
|
+
return suffixed.length > SLACK_COMMAND_MAX_LENGTH ? base : suffixed;
|
|
15832
|
+
}
|
|
15833
|
+
function matchesAgentCommand(command, base) {
|
|
15834
|
+
return command === base || command === agentSlashCommand(base);
|
|
15835
|
+
}
|
|
15512
15836
|
var AGT_HOST = process.env.AGT_HOST ?? null;
|
|
15513
15837
|
var AGT_API_KEY = process.env.AGT_API_KEY ?? null;
|
|
15514
15838
|
var AGT_AGENT_ID = process.env.AGT_AGENT_ID ?? null;
|
|
15515
15839
|
var AGT_TEAM_ID = process.env.AGT_TEAM_ID ?? null;
|
|
15516
15840
|
var SLACK_SENDER_POLICY = (() => {
|
|
15517
15841
|
const raw = (process.env.SLACK_SENDER_POLICY ?? "all").trim().toLowerCase();
|
|
15518
|
-
|
|
15519
|
-
|
|
15842
|
+
const internalOnly = process.env.SLACK_INTERNAL_ONLY === "true";
|
|
15843
|
+
const homeTeamId = process.env.SLACK_HOME_TEAM_ID;
|
|
15844
|
+
if (internalOnly && !homeTeamId) {
|
|
15845
|
+
throw new Error(
|
|
15846
|
+
"SLACK_INTERNAL_ONLY=true requires SLACK_HOME_TEAM_ID. The bot install must have a known team_id (run auth.test against the workspace and persist the result)."
|
|
15847
|
+
);
|
|
15848
|
+
}
|
|
15849
|
+
const internalOnlyFields = internalOnly ? { internalOnly: true, homeTeamId } : {};
|
|
15850
|
+
if (raw === "all") return { mode: "all", ...internalOnlyFields };
|
|
15851
|
+
if (raw === "agents_only") return { mode: "agents_only", ...internalOnlyFields };
|
|
15520
15852
|
if (raw === "team_agents_only") {
|
|
15521
15853
|
if (!AGT_TEAM_ID) {
|
|
15522
15854
|
throw new Error("SLACK_SENDER_POLICY=team_agents_only requires AGT_TEAM_ID");
|
|
15523
15855
|
}
|
|
15524
|
-
return { mode: "team_agents_only", teamId: AGT_TEAM_ID };
|
|
15856
|
+
return { mode: "team_agents_only", teamId: AGT_TEAM_ID, ...internalOnlyFields };
|
|
15857
|
+
}
|
|
15858
|
+
if (raw === "team_only") {
|
|
15859
|
+
if (!AGT_TEAM_ID) {
|
|
15860
|
+
throw new Error("SLACK_SENDER_POLICY=team_only requires AGT_TEAM_ID");
|
|
15861
|
+
}
|
|
15862
|
+
const rawIds = process.env.SLACK_SENDER_POLICY_TEAM_PRINCIPAL_IDS ?? "";
|
|
15863
|
+
const teamPrincipalSlackUserIds = rawIds.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
15864
|
+
return {
|
|
15865
|
+
mode: "team_only",
|
|
15866
|
+
teamId: AGT_TEAM_ID,
|
|
15867
|
+
teamPrincipalSlackUserIds,
|
|
15868
|
+
...internalOnlyFields
|
|
15869
|
+
};
|
|
15870
|
+
}
|
|
15871
|
+
if (raw === "manager_only") {
|
|
15872
|
+
const principalSlackUserId = process.env.SLACK_SENDER_POLICY_PRINCIPAL_ID;
|
|
15873
|
+
if (!AGT_TEAM_ID) {
|
|
15874
|
+
throw new Error("SLACK_SENDER_POLICY=manager_only requires AGT_TEAM_ID");
|
|
15875
|
+
}
|
|
15876
|
+
if (!principalSlackUserId) {
|
|
15877
|
+
throw new Error(
|
|
15878
|
+
"SLACK_SENDER_POLICY=manager_only requires SLACK_SENDER_POLICY_PRINCIPAL_ID. Set agent.reports_to_person with a slack_user_id in contact_preferences and re-run /host/refresh."
|
|
15879
|
+
);
|
|
15880
|
+
}
|
|
15881
|
+
return { mode: "manager_only", teamId: AGT_TEAM_ID, principalSlackUserId, ...internalOnlyFields };
|
|
15525
15882
|
}
|
|
15526
15883
|
throw new Error(`Invalid SLACK_SENDER_POLICY=${JSON.stringify(process.env.SLACK_SENDER_POLICY)}`);
|
|
15527
15884
|
})();
|
|
15885
|
+
var SLACK_POLICY_DECLINE_CACHE = /* @__PURE__ */ new Map();
|
|
15886
|
+
var SLACK_POLICY_DECLINE_COOLDOWN_MS = readDeclineCooldownMs(
|
|
15887
|
+
"SLACK_SENDER_POLICY_REPLY_COOLDOWN_SEC"
|
|
15888
|
+
);
|
|
15889
|
+
async function maybeSendSenderPolicyDecline(args) {
|
|
15890
|
+
if (!BOT_TOKEN) return;
|
|
15891
|
+
if (!args.channel || !args.senderId) return;
|
|
15892
|
+
const key2 = declineCacheKey(args.channel, args.senderId, args.subReason);
|
|
15893
|
+
const decision = decideDeclineReply({
|
|
15894
|
+
cache: SLACK_POLICY_DECLINE_CACHE,
|
|
15895
|
+
key: key2,
|
|
15896
|
+
cooldownMs: SLACK_POLICY_DECLINE_COOLDOWN_MS,
|
|
15897
|
+
now: Date.now()
|
|
15898
|
+
});
|
|
15899
|
+
if (!decision.reply) {
|
|
15900
|
+
process.stderr.write(
|
|
15901
|
+
`slack-channel(${AGENT_CODE_NAME}): decline suppressed by cooldown (channel=${redactSlackId(args.channel)}, sender=${redactSlackId(args.senderId)}, reason=${args.subReason}, remaining=${decision.remainingMs}ms)
|
|
15902
|
+
`
|
|
15903
|
+
);
|
|
15904
|
+
return;
|
|
15905
|
+
}
|
|
15906
|
+
const text = politeDeclineCopy(args.subReason);
|
|
15907
|
+
const controller = new AbortController();
|
|
15908
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
15909
|
+
try {
|
|
15910
|
+
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
15911
|
+
method: "POST",
|
|
15912
|
+
signal: controller.signal,
|
|
15913
|
+
headers: {
|
|
15914
|
+
"Content-Type": "application/json",
|
|
15915
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
15916
|
+
},
|
|
15917
|
+
body: JSON.stringify({
|
|
15918
|
+
channel: args.channel,
|
|
15919
|
+
text,
|
|
15920
|
+
// Reply in-thread if the inbound was a thread message — keeps
|
|
15921
|
+
// the decline next to the message that caused it. For top-level
|
|
15922
|
+
// posts (DMs / channel root) omit thread_ts and post inline.
|
|
15923
|
+
...args.threadTs ? { thread_ts: args.threadTs } : {}
|
|
15924
|
+
})
|
|
15925
|
+
});
|
|
15926
|
+
const data = await res.json();
|
|
15927
|
+
if (!data.ok) {
|
|
15928
|
+
process.stderr.write(
|
|
15929
|
+
`slack-channel(${AGENT_CODE_NAME}): decline post failed: ${data.error ?? "unknown"}
|
|
15930
|
+
`
|
|
15931
|
+
);
|
|
15932
|
+
}
|
|
15933
|
+
} finally {
|
|
15934
|
+
clearTimeout(timeoutId);
|
|
15935
|
+
}
|
|
15936
|
+
}
|
|
15528
15937
|
var BLOCK_KIT_ENABLED = process.env.SLACK_BLOCK_KIT_ENABLED === "true";
|
|
15529
15938
|
var BLOCK_KIT_ASK_USER_ENABLED = process.env.SLACK_BLOCK_KIT_ASK_USER_ENABLED === "true";
|
|
15530
15939
|
var BLOCK_KIT_DISABLED = process.env.SLACK_BLOCK_KIT_DISABLED === "true";
|
|
@@ -15576,9 +15985,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
|
|
|
15576
15985
|
peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
|
|
15577
15986
|
peer_disabled_mode: SLACK_PEER_DISABLED_MODE
|
|
15578
15987
|
};
|
|
15579
|
-
var SLACK_AGENT_DIR = AGENT_CODE_NAME ?
|
|
15580
|
-
var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ?
|
|
15581
|
-
var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ?
|
|
15988
|
+
var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join5(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
|
|
15989
|
+
var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join5(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
|
|
15990
|
+
var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join5(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
|
|
15582
15991
|
var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
|
|
15583
15992
|
function redactSlackId(id) {
|
|
15584
15993
|
if (!id) return "<none>";
|
|
@@ -15590,7 +15999,7 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
|
|
|
15590
15999
|
}
|
|
15591
16000
|
function slackPendingInboundPath(channel, threadTs, messageTs) {
|
|
15592
16001
|
if (!SLACK_PENDING_INBOUND_DIR) return null;
|
|
15593
|
-
return
|
|
16002
|
+
return join5(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
|
|
15594
16003
|
}
|
|
15595
16004
|
function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable = false) {
|
|
15596
16005
|
const path = slackPendingInboundPath(channel, threadTs, messageTs);
|
|
@@ -15619,12 +16028,16 @@ function healSlackUndeliverable(channel, messageTs) {
|
|
|
15619
16028
|
"Content-Type": "application/json",
|
|
15620
16029
|
Authorization: `Bearer ${BOT_TOKEN}`
|
|
15621
16030
|
};
|
|
15622
|
-
fetch("https://slack.com/api/reactions.remove", {
|
|
16031
|
+
const remove = (name) => fetch("https://slack.com/api/reactions.remove", {
|
|
15623
16032
|
method: "POST",
|
|
15624
16033
|
headers,
|
|
15625
|
-
body: JSON.stringify({ channel, timestamp: messageTs, name
|
|
16034
|
+
body: JSON.stringify({ channel, timestamp: messageTs, name })
|
|
15626
16035
|
}).catch(() => {
|
|
15627
|
-
})
|
|
16036
|
+
});
|
|
16037
|
+
void Promise.allSettled([
|
|
16038
|
+
remove(SLACK_UNDELIVERABLE_REACTION),
|
|
16039
|
+
remove(SLACK_UNDELIVERABLE_REACTION_LEGACY)
|
|
16040
|
+
]).finally(() => {
|
|
15628
16041
|
fetch("https://slack.com/api/reactions.add", {
|
|
15629
16042
|
method: "POST",
|
|
15630
16043
|
headers,
|
|
@@ -15633,6 +16046,32 @@ function healSlackUndeliverable(channel, messageTs) {
|
|
|
15633
16046
|
});
|
|
15634
16047
|
});
|
|
15635
16048
|
}
|
|
16049
|
+
var lastSlackUndeliverableNoticeAt = /* @__PURE__ */ new Map();
|
|
16050
|
+
function postUndeliverableNotice(channel, threadTs, isThreadReply) {
|
|
16051
|
+
if (!BOT_TOKEN || !channel) return;
|
|
16052
|
+
const now = Date.now();
|
|
16053
|
+
const conversationKey = isThreadReply && threadTs ? `${channel}:${threadTs}` : channel;
|
|
16054
|
+
if (!shouldPostUndeliverableNotice(lastSlackUndeliverableNoticeAt.get(conversationKey), now)) {
|
|
16055
|
+
return;
|
|
16056
|
+
}
|
|
16057
|
+
lastSlackUndeliverableNoticeAt.set(conversationKey, now);
|
|
16058
|
+
fetch("https://slack.com/api/chat.postMessage", {
|
|
16059
|
+
method: "POST",
|
|
16060
|
+
headers: {
|
|
16061
|
+
"Content-Type": "application/json",
|
|
16062
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
16063
|
+
},
|
|
16064
|
+
body: JSON.stringify({
|
|
16065
|
+
channel,
|
|
16066
|
+
text: undeliverableNoticeText(),
|
|
16067
|
+
...threadTs ? { thread_ts: threadTs } : {}
|
|
16068
|
+
})
|
|
16069
|
+
}).catch(() => {
|
|
16070
|
+
});
|
|
16071
|
+
}
|
|
16072
|
+
function __resetSlackUndeliverableNoticeThrottle() {
|
|
16073
|
+
lastSlackUndeliverableNoticeAt.clear();
|
|
16074
|
+
}
|
|
15636
16075
|
function clearSlackMarkerFileWithHeal(fullPath) {
|
|
15637
16076
|
let marker = null;
|
|
15638
16077
|
try {
|
|
@@ -15646,22 +16085,32 @@ function clearSlackMarkerFileWithHeal(fullPath) {
|
|
|
15646
16085
|
healSlackUndeliverable(marker.channel, marker.message_ts);
|
|
15647
16086
|
}
|
|
15648
16087
|
try {
|
|
15649
|
-
if (
|
|
16088
|
+
if (existsSync3(fullPath)) unlinkSync3(fullPath);
|
|
15650
16089
|
} catch {
|
|
15651
16090
|
}
|
|
15652
16091
|
}
|
|
15653
|
-
function
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
|
|
15658
|
-
|
|
15659
|
-
|
|
15660
|
-
|
|
15661
|
-
|
|
15662
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
16092
|
+
function clearAllSlackPendingMarkersForThread2(channel, threadTs) {
|
|
16093
|
+
clearAllSlackPendingMarkersForThread(
|
|
16094
|
+
SLACK_PENDING_INBOUND_DIR,
|
|
16095
|
+
channel,
|
|
16096
|
+
threadTs,
|
|
16097
|
+
clearSlackMarkerFileWithHeal
|
|
16098
|
+
);
|
|
16099
|
+
}
|
|
16100
|
+
function clearSlackPendingMarkerByMessageTs2(channel, messageTs) {
|
|
16101
|
+
clearSlackPendingMarkerByMessageTs(
|
|
16102
|
+
SLACK_PENDING_INBOUND_DIR,
|
|
16103
|
+
channel,
|
|
16104
|
+
messageTs,
|
|
16105
|
+
clearSlackMarkerFileWithHeal
|
|
16106
|
+
);
|
|
16107
|
+
}
|
|
16108
|
+
function clearOldestSlackPendingMarkerInChannel2(channel) {
|
|
16109
|
+
clearOldestSlackPendingMarkerInChannel(
|
|
16110
|
+
SLACK_PENDING_INBOUND_DIR,
|
|
16111
|
+
channel,
|
|
16112
|
+
clearSlackMarkerFileWithHeal
|
|
16113
|
+
);
|
|
15665
16114
|
}
|
|
15666
16115
|
function slackNextRetryName(filename) {
|
|
15667
16116
|
const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
|
|
@@ -15677,7 +16126,7 @@ function slackNextRetryName(filename) {
|
|
|
15677
16126
|
async function processSlackRecoveryOutboxFile(filename) {
|
|
15678
16127
|
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
15679
16128
|
if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
|
|
15680
|
-
const fullPath =
|
|
16129
|
+
const fullPath = join5(SLACK_RECOVERY_OUTBOX_DIR, filename);
|
|
15681
16130
|
let payload;
|
|
15682
16131
|
try {
|
|
15683
16132
|
payload = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
@@ -15746,7 +16195,7 @@ async function processSlackRecoveryOutboxFile(filename) {
|
|
|
15746
16195
|
}
|
|
15747
16196
|
if (sendSucceeded) {
|
|
15748
16197
|
try {
|
|
15749
|
-
|
|
16198
|
+
unlinkSync3(fullPath);
|
|
15750
16199
|
} catch {
|
|
15751
16200
|
}
|
|
15752
16201
|
return;
|
|
@@ -15754,7 +16203,7 @@ async function processSlackRecoveryOutboxFile(filename) {
|
|
|
15754
16203
|
const next = slackNextRetryName(filename);
|
|
15755
16204
|
if (next) {
|
|
15756
16205
|
try {
|
|
15757
|
-
renameSync2(fullPath,
|
|
16206
|
+
renameSync2(fullPath, join5(SLACK_RECOVERY_OUTBOX_DIR, next.next));
|
|
15758
16207
|
if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
|
|
15759
16208
|
process.stderr.write(
|
|
15760
16209
|
`slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
|
|
@@ -15784,7 +16233,7 @@ function scanSlackRecoveryRetries() {
|
|
|
15784
16233
|
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
15785
16234
|
let entries;
|
|
15786
16235
|
try {
|
|
15787
|
-
entries =
|
|
16236
|
+
entries = readdirSync3(SLACK_RECOVERY_OUTBOX_DIR);
|
|
15788
16237
|
} catch {
|
|
15789
16238
|
return;
|
|
15790
16239
|
}
|
|
@@ -15793,7 +16242,7 @@ function scanSlackRecoveryRetries() {
|
|
|
15793
16242
|
if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
|
|
15794
16243
|
let mtimeMs;
|
|
15795
16244
|
try {
|
|
15796
|
-
mtimeMs =
|
|
16245
|
+
mtimeMs = statSync2(join5(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
|
|
15797
16246
|
} catch {
|
|
15798
16247
|
continue;
|
|
15799
16248
|
}
|
|
@@ -15814,7 +16263,7 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
15814
16263
|
return;
|
|
15815
16264
|
}
|
|
15816
16265
|
try {
|
|
15817
|
-
for (const f of
|
|
16266
|
+
for (const f of readdirSync3(SLACK_RECOVERY_OUTBOX_DIR)) {
|
|
15818
16267
|
if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
|
|
15819
16268
|
}
|
|
15820
16269
|
} catch {
|
|
@@ -15823,7 +16272,7 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
15823
16272
|
const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
|
|
15824
16273
|
if (event !== "rename" || !filename) return;
|
|
15825
16274
|
if (!isFirstAttemptSlackOutboxFile(filename)) return;
|
|
15826
|
-
if (
|
|
16275
|
+
if (existsSync3(join5(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
|
|
15827
16276
|
void processSlackRecoveryOutboxFile(filename);
|
|
15828
16277
|
}
|
|
15829
16278
|
});
|
|
@@ -15842,12 +16291,12 @@ var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
|
|
|
15842
16291
|
function trackPendingMessage(channel, threadTs, messageTs, undeliverable = false) {
|
|
15843
16292
|
writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable);
|
|
15844
16293
|
}
|
|
15845
|
-
function
|
|
16294
|
+
function sweepSlackStaleMarkers(thresholdMs) {
|
|
15846
16295
|
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
15847
|
-
if (!
|
|
16296
|
+
if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
|
|
15848
16297
|
let filenames;
|
|
15849
16298
|
try {
|
|
15850
|
-
filenames =
|
|
16299
|
+
filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
|
|
15851
16300
|
} catch (err) {
|
|
15852
16301
|
process.stderr.write(
|
|
15853
16302
|
`slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
|
|
@@ -15860,7 +16309,7 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
15860
16309
|
for (const filename of filenames) {
|
|
15861
16310
|
if (!filename.endsWith(".json")) continue;
|
|
15862
16311
|
if (filename.endsWith(".tmp")) continue;
|
|
15863
|
-
const fullPath =
|
|
16312
|
+
const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
|
|
15864
16313
|
let marker;
|
|
15865
16314
|
try {
|
|
15866
16315
|
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
@@ -15870,25 +16319,16 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
15870
16319
|
`
|
|
15871
16320
|
);
|
|
15872
16321
|
try {
|
|
15873
|
-
|
|
16322
|
+
unlinkSync3(fullPath);
|
|
15874
16323
|
} catch {
|
|
15875
16324
|
}
|
|
15876
16325
|
cleared++;
|
|
15877
16326
|
continue;
|
|
15878
16327
|
}
|
|
15879
16328
|
const { channel, thread_ts, message_ts, received_at } = marker;
|
|
15880
|
-
if (!channel || !thread_ts || !message_ts ||
|
|
15881
|
-
try {
|
|
15882
|
-
unlinkSync2(fullPath);
|
|
15883
|
-
} catch {
|
|
15884
|
-
}
|
|
15885
|
-
cleared++;
|
|
15886
|
-
continue;
|
|
15887
|
-
}
|
|
15888
|
-
const receivedAtMs = Date.parse(received_at);
|
|
15889
|
-
if (!Number.isFinite(receivedAtMs) || receivedAtMs > now || now - receivedAtMs > STALE_MARKER_MS) {
|
|
16329
|
+
if (!channel || !thread_ts || !message_ts || isPendingMarkerStale(received_at, now, thresholdMs)) {
|
|
15890
16330
|
try {
|
|
15891
|
-
|
|
16331
|
+
unlinkSync3(fullPath);
|
|
15892
16332
|
} catch {
|
|
15893
16333
|
}
|
|
15894
16334
|
cleared++;
|
|
@@ -15901,9 +16341,167 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
15901
16341
|
);
|
|
15902
16342
|
}
|
|
15903
16343
|
}
|
|
15904
|
-
|
|
16344
|
+
sweepSlackStaleMarkers(STALE_MARKER_MS);
|
|
16345
|
+
var slackOrphanSweepTimer = setInterval(() => {
|
|
16346
|
+
const probe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
|
|
16347
|
+
const sessionAlive = probe.tmux === "alive" && probe.claude === "alive";
|
|
16348
|
+
sweepSlackStaleMarkers(sessionAlive ? channelOrphanMarkerMs() : STALE_MARKER_MS);
|
|
16349
|
+
checkSlackWatchdogGiveUpNotice();
|
|
16350
|
+
}, orphanSweepIntervalMs());
|
|
16351
|
+
slackOrphanSweepTimer.unref?.();
|
|
16352
|
+
var lastSlackGiveUpHandledAtMs = null;
|
|
16353
|
+
function listPendingSlackConversations() {
|
|
16354
|
+
if (!SLACK_PENDING_INBOUND_DIR || !existsSync3(SLACK_PENDING_INBOUND_DIR)) return [];
|
|
16355
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
16356
|
+
try {
|
|
16357
|
+
for (const name of readdirSync3(SLACK_PENDING_INBOUND_DIR)) {
|
|
16358
|
+
if (!name.endsWith(".json")) continue;
|
|
16359
|
+
try {
|
|
16360
|
+
const marker = JSON.parse(
|
|
16361
|
+
readFileSync4(join5(SLACK_PENDING_INBOUND_DIR, name), "utf8")
|
|
16362
|
+
);
|
|
16363
|
+
if (typeof marker.channel !== "string" || !marker.channel) continue;
|
|
16364
|
+
if (typeof marker.thread_ts !== "string" || !marker.thread_ts) continue;
|
|
16365
|
+
const isThreadReply = typeof marker.message_ts === "string" && marker.message_ts !== marker.thread_ts;
|
|
16366
|
+
const key2 = isThreadReply ? `${marker.channel}:${marker.thread_ts}` : marker.channel;
|
|
16367
|
+
if (!byKey.has(key2)) {
|
|
16368
|
+
byKey.set(key2, { channel: marker.channel, threadTs: marker.thread_ts, isThreadReply });
|
|
16369
|
+
}
|
|
16370
|
+
} catch {
|
|
16371
|
+
}
|
|
16372
|
+
}
|
|
16373
|
+
} catch {
|
|
16374
|
+
return [];
|
|
16375
|
+
}
|
|
16376
|
+
return [...byKey.values()];
|
|
16377
|
+
}
|
|
16378
|
+
function postSlackWatchdogGiveUpNotice(channel, threadTs, isThreadReply) {
|
|
16379
|
+
if (!BOT_TOKEN || !channel) return;
|
|
16380
|
+
const now = Date.now();
|
|
16381
|
+
const conversationKey = isThreadReply ? `${channel}:${threadTs}` : channel;
|
|
16382
|
+
if (!shouldPostUndeliverableNotice(lastSlackUndeliverableNoticeAt.get(conversationKey), now)) {
|
|
16383
|
+
return;
|
|
16384
|
+
}
|
|
16385
|
+
lastSlackUndeliverableNoticeAt.set(conversationKey, now);
|
|
16386
|
+
process.stderr.write(
|
|
16387
|
+
`slack-channel(${AGENT_CODE_NAME}): give-up notice posted channel=${redactSlackId(channel)}
|
|
16388
|
+
`
|
|
16389
|
+
);
|
|
16390
|
+
fetch("https://slack.com/api/chat.postMessage", {
|
|
16391
|
+
method: "POST",
|
|
16392
|
+
headers: {
|
|
16393
|
+
"Content-Type": "application/json",
|
|
16394
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
16395
|
+
},
|
|
16396
|
+
body: JSON.stringify({
|
|
16397
|
+
channel,
|
|
16398
|
+
text: giveUpNoticeText(),
|
|
16399
|
+
// CR on PR #1824: anchor to the originating message even for root
|
|
16400
|
+
// messages (threadTs === messageTs is still the conversation target) —
|
|
16401
|
+
// a channel-root notice diverges from postUndeliverableNotice and
|
|
16402
|
+
// loses the link to the message the user should resend. isThreadReply
|
|
16403
|
+
// only shapes the THROTTLE key, not the delivery target.
|
|
16404
|
+
...threadTs ? { thread_ts: threadTs } : {}
|
|
16405
|
+
})
|
|
16406
|
+
}).catch(() => {
|
|
16407
|
+
});
|
|
16408
|
+
}
|
|
16409
|
+
function checkSlackWatchdogGiveUpNotice() {
|
|
16410
|
+
if (!SLACK_AGENT_DIR) return;
|
|
16411
|
+
const signalAtMs = readGiveUpSignalAtMs(join5(SLACK_AGENT_DIR, GIVE_UP_SIGNAL_FILENAME));
|
|
16412
|
+
const act = decideGiveUpNotice({
|
|
16413
|
+
signalAtMs,
|
|
16414
|
+
lastHandledAtMs: lastSlackGiveUpHandledAtMs,
|
|
16415
|
+
nowMs: Date.now()
|
|
16416
|
+
});
|
|
16417
|
+
if (signalAtMs != null) {
|
|
16418
|
+
lastSlackGiveUpHandledAtMs = Math.max(lastSlackGiveUpHandledAtMs ?? 0, signalAtMs);
|
|
16419
|
+
}
|
|
16420
|
+
if (!act) return;
|
|
16421
|
+
for (const convo of listPendingSlackConversations()) {
|
|
16422
|
+
postSlackWatchdogGiveUpNotice(convo.channel, convo.threadTs, convo.isThreadReply);
|
|
16423
|
+
}
|
|
16424
|
+
}
|
|
16425
|
+
function __resetSlackGiveUpNoticeStateForTests() {
|
|
16426
|
+
lastSlackGiveUpHandledAtMs = null;
|
|
16427
|
+
}
|
|
16428
|
+
var SLACK_PROCESS_BOOT_MS = Date.now();
|
|
16429
|
+
var STRANDED_INBOUND_MIN_AGE_FROM_BOOT_MS = 6e4;
|
|
16430
|
+
var STRANDED_INBOUND_MAX_AGE_MS = 5 * 60 * 1e3;
|
|
16431
|
+
var strandedInboundNoticeFired = false;
|
|
16432
|
+
var strandedInboundNoticeInFlight = false;
|
|
16433
|
+
async function notifyStrandedInboundsOnFirstConnect() {
|
|
16434
|
+
if (strandedInboundNoticeFired || strandedInboundNoticeInFlight) return;
|
|
16435
|
+
strandedInboundNoticeInFlight = true;
|
|
16436
|
+
let hadFailure = false;
|
|
16437
|
+
try {
|
|
16438
|
+
if (!SLACK_PENDING_INBOUND_DIR || !existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
|
|
16439
|
+
let filenames;
|
|
16440
|
+
try {
|
|
16441
|
+
filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
|
|
16442
|
+
} catch {
|
|
16443
|
+
hadFailure = true;
|
|
16444
|
+
return;
|
|
16445
|
+
}
|
|
16446
|
+
const now = Date.now();
|
|
16447
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16448
|
+
let notified = 0;
|
|
16449
|
+
for (const filename of filenames) {
|
|
16450
|
+
if (!filename.endsWith(".json")) continue;
|
|
16451
|
+
const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
|
|
16452
|
+
let marker;
|
|
16453
|
+
try {
|
|
16454
|
+
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
16455
|
+
} catch {
|
|
16456
|
+
continue;
|
|
16457
|
+
}
|
|
16458
|
+
const { channel, thread_ts, received_at } = marker;
|
|
16459
|
+
if (!channel || !thread_ts || !received_at) continue;
|
|
16460
|
+
const receivedAtMs = Date.parse(received_at);
|
|
16461
|
+
if (!Number.isFinite(receivedAtMs)) continue;
|
|
16462
|
+
if (receivedAtMs >= SLACK_PROCESS_BOOT_MS - STRANDED_INBOUND_MIN_AGE_FROM_BOOT_MS) continue;
|
|
16463
|
+
if (now - receivedAtMs > STRANDED_INBOUND_MAX_AGE_MS) continue;
|
|
16464
|
+
const key2 = `${channel}__${thread_ts}`;
|
|
16465
|
+
if (seen.has(key2)) {
|
|
16466
|
+
try {
|
|
16467
|
+
unlinkSync3(fullPath);
|
|
16468
|
+
} catch {
|
|
16469
|
+
}
|
|
16470
|
+
continue;
|
|
16471
|
+
}
|
|
16472
|
+
seen.add(key2);
|
|
16473
|
+
const res = await postSlackMessage({
|
|
16474
|
+
channel,
|
|
16475
|
+
thread_ts,
|
|
16476
|
+
text: "I was restarted mid-conversation and may have missed your last message. Please re-send if I didn't reply."
|
|
16477
|
+
});
|
|
16478
|
+
if (res.ok) {
|
|
16479
|
+
notified += 1;
|
|
16480
|
+
try {
|
|
16481
|
+
unlinkSync3(fullPath);
|
|
16482
|
+
} catch {
|
|
16483
|
+
}
|
|
16484
|
+
} else {
|
|
16485
|
+
hadFailure = true;
|
|
16486
|
+
process.stderr.write(
|
|
16487
|
+
`slack-channel(${AGENT_CODE_NAME}): stranded-inbound notify failed channel=${redactSlackId(channel)} thread=${redactSlackId(thread_ts)} error=${res.error}
|
|
16488
|
+
`
|
|
16489
|
+
);
|
|
16490
|
+
}
|
|
16491
|
+
}
|
|
16492
|
+
if (notified > 0) {
|
|
16493
|
+
process.stderr.write(
|
|
16494
|
+
`slack-channel(${AGENT_CODE_NAME}): notified ${notified} stranded inbound thread(s) after restart
|
|
16495
|
+
`
|
|
16496
|
+
);
|
|
16497
|
+
}
|
|
16498
|
+
} finally {
|
|
16499
|
+
if (!hadFailure) strandedInboundNoticeFired = true;
|
|
16500
|
+
strandedInboundNoticeInFlight = false;
|
|
16501
|
+
}
|
|
16502
|
+
}
|
|
15905
16503
|
function clearPendingMessage(channel, threadTs) {
|
|
15906
|
-
|
|
16504
|
+
clearAllSlackPendingMarkersForThread2(channel, threadTs);
|
|
15907
16505
|
}
|
|
15908
16506
|
function noteThreadActivity(channel, threadTs) {
|
|
15909
16507
|
if (!channel || !threadTs) return;
|
|
@@ -15913,10 +16511,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
|
|
|
15913
16511
|
if (!channel || !messageTs) return;
|
|
15914
16512
|
clearPendingMessage(channel, messageTs);
|
|
15915
16513
|
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
15916
|
-
if (!
|
|
16514
|
+
if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
|
|
15917
16515
|
let filenames;
|
|
15918
16516
|
try {
|
|
15919
|
-
filenames =
|
|
16517
|
+
filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
|
|
15920
16518
|
} catch {
|
|
15921
16519
|
return;
|
|
15922
16520
|
}
|
|
@@ -15927,10 +16525,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
|
|
|
15927
16525
|
for (const filename of filenames) {
|
|
15928
16526
|
if (!filename.startsWith(channelPrefix)) continue;
|
|
15929
16527
|
if (!filename.endsWith(messageSuffix)) continue;
|
|
15930
|
-
clearSlackMarkerFileWithHeal(
|
|
16528
|
+
clearSlackMarkerFileWithHeal(join5(SLACK_PENDING_INBOUND_DIR, filename));
|
|
15931
16529
|
}
|
|
15932
16530
|
}
|
|
15933
|
-
var RESTART_FLAGS_DIR =
|
|
16531
|
+
var RESTART_FLAGS_DIR = join5(homedir2(), ".augmented", "restart-flags");
|
|
15934
16532
|
function buildAugmentedSlackMetadata() {
|
|
15935
16533
|
if (!AGT_TEAM_ID) return void 0;
|
|
15936
16534
|
return {
|
|
@@ -15980,12 +16578,13 @@ function buildSlackHelpMessage(codeName) {
|
|
|
15980
16578
|
return [
|
|
15981
16579
|
`\u{1F916} *Available commands for \`${codeName}\`*`,
|
|
15982
16580
|
"",
|
|
15983
|
-
"All commands are real Slack slash commands (autocomplete via `/`). For backward compatibility, `/help` and
|
|
16581
|
+
"All commands are real Slack slash commands (autocomplete via `/`). For backward compatibility, `/help` and the restart command can also be typed as plain messages in any channel where the bot is present:",
|
|
15984
16582
|
"\u2022 `/help` \u2014 show this help",
|
|
15985
|
-
|
|
15986
|
-
|
|
16583
|
+
`\u2022 \`${agentSlashCommand("/restart")}\` \u2014 restart this agent`,
|
|
16584
|
+
`\u2022 \`${agentSlashCommand("/agent-status")}\` \u2014 report whether this agent is online + last activity`,
|
|
15987
16585
|
"\u2022 `/kill` \u2014 silence all agents in this thread for 6h (use as a thread reply)",
|
|
15988
|
-
"\u2022 `/unkill` \u2014 clear a kill (use as a thread reply)"
|
|
16586
|
+
"\u2022 `/unkill` \u2014 clear a kill (use as a thread reply)",
|
|
16587
|
+
`\u2022 \`${agentSlashCommand("/investigate")}\` \u2014 live tail of this agent's terminal pane (DM only, allowlisted users; works while the channel process is alive \u2014 a wedged host still needs SSM diagnostics)`
|
|
15989
16588
|
].join("\n");
|
|
15990
16589
|
}
|
|
15991
16590
|
var lastActivityAt = null;
|
|
@@ -16074,12 +16673,202 @@ async function postEphemeralViaResponseUrl(responseUrl, text, logTag) {
|
|
|
16074
16673
|
);
|
|
16075
16674
|
}
|
|
16076
16675
|
}
|
|
16676
|
+
var DEBUG_TAIL_WINDOW_MS = 12e4;
|
|
16677
|
+
var DEBUG_TAIL_INTERVAL_MS = 3e3;
|
|
16678
|
+
var DEBUG_TAIL_MAX_CONSECUTIVE_FAILURES = 4;
|
|
16679
|
+
var activeDebugTailExpiresAtMs = null;
|
|
16680
|
+
function debugAuditLog(line) {
|
|
16681
|
+
process.stderr.write(`slack-channel(${AGENT_CODE_NAME ?? "unknown"}): /debug ${line}
|
|
16682
|
+
`);
|
|
16683
|
+
}
|
|
16684
|
+
function debugSleep(ms) {
|
|
16685
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
16686
|
+
}
|
|
16687
|
+
function nowUtcLabel() {
|
|
16688
|
+
return `${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} UTC`;
|
|
16689
|
+
}
|
|
16690
|
+
async function updateSlackMessage(channel, ts, text) {
|
|
16691
|
+
try {
|
|
16692
|
+
const res = await fetch("https://slack.com/api/chat.update", {
|
|
16693
|
+
method: "POST",
|
|
16694
|
+
headers: {
|
|
16695
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
16696
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
16697
|
+
},
|
|
16698
|
+
body: JSON.stringify({ channel, ts, text }),
|
|
16699
|
+
signal: AbortSignal.timeout(SLACK_DOWNLOAD_TIMEOUT_MS)
|
|
16700
|
+
});
|
|
16701
|
+
const retryAfterHeader = res.headers.get("retry-after");
|
|
16702
|
+
const retryAfterSec = retryAfterHeader ? parseInt(retryAfterHeader, 10) : NaN;
|
|
16703
|
+
const retryAfterMs = Number.isFinite(retryAfterSec) ? Math.max(0, retryAfterSec * 1e3) : null;
|
|
16704
|
+
const data = await res.json().catch(() => ({ ok: false, error: `http-${res.status}` }));
|
|
16705
|
+
return { ok: data.ok === true, error: data.error, retryAfterMs };
|
|
16706
|
+
} catch (err) {
|
|
16707
|
+
return { ok: false, error: err.message, retryAfterMs: null };
|
|
16708
|
+
}
|
|
16709
|
+
}
|
|
16710
|
+
async function runDebugTailLoop(opts) {
|
|
16711
|
+
const expiresAtMs = opts.expiresAtMs;
|
|
16712
|
+
let last = opts.initialFrame;
|
|
16713
|
+
let consecutiveFailures = 0;
|
|
16714
|
+
let delayMs = DEBUG_TAIL_INTERVAL_MS;
|
|
16715
|
+
try {
|
|
16716
|
+
while (Date.now() + delayMs < expiresAtMs) {
|
|
16717
|
+
await debugSleep(delayMs);
|
|
16718
|
+
delayMs = DEBUG_TAIL_INTERVAL_MS;
|
|
16719
|
+
const snapshot = await capturePaneSnapshot({
|
|
16720
|
+
codeName: opts.codeName,
|
|
16721
|
+
agentDir: SLACK_AGENT_DIR
|
|
16722
|
+
});
|
|
16723
|
+
if (!snapshot) continue;
|
|
16724
|
+
const body = redactPaneText(snapshot.text);
|
|
16725
|
+
if (body === last.body) continue;
|
|
16726
|
+
const frame = { body, source: snapshot.source, capturedAtLabel: nowUtcLabel() };
|
|
16727
|
+
const secondsRemaining = Math.max(0, Math.round((expiresAtMs - Date.now()) / 1e3));
|
|
16728
|
+
const result = await updateSlackMessage(
|
|
16729
|
+
opts.channel,
|
|
16730
|
+
opts.ts,
|
|
16731
|
+
formatPaneSnapshot({
|
|
16732
|
+
codeName: opts.codeName,
|
|
16733
|
+
text: frame.body,
|
|
16734
|
+
source: frame.source,
|
|
16735
|
+
capturedAtLabel: frame.capturedAtLabel,
|
|
16736
|
+
status: { kind: "live", secondsRemaining }
|
|
16737
|
+
})
|
|
16738
|
+
);
|
|
16739
|
+
if (result.ok) {
|
|
16740
|
+
last = frame;
|
|
16741
|
+
consecutiveFailures = 0;
|
|
16742
|
+
continue;
|
|
16743
|
+
}
|
|
16744
|
+
if (result.retryAfterMs !== null || result.error === "ratelimited") {
|
|
16745
|
+
delayMs = Math.max(result.retryAfterMs ?? 0, DEBUG_TAIL_INTERVAL_MS * 2);
|
|
16746
|
+
continue;
|
|
16747
|
+
}
|
|
16748
|
+
consecutiveFailures++;
|
|
16749
|
+
if (consecutiveFailures >= DEBUG_TAIL_MAX_CONSECUTIVE_FAILURES) {
|
|
16750
|
+
debugAuditLog(
|
|
16751
|
+
`tail aborted after ${consecutiveFailures} consecutive update failures (last: ${result.error ?? "unknown"})`
|
|
16752
|
+
);
|
|
16753
|
+
break;
|
|
16754
|
+
}
|
|
16755
|
+
}
|
|
16756
|
+
} finally {
|
|
16757
|
+
activeDebugTailExpiresAtMs = null;
|
|
16758
|
+
await updateSlackMessage(
|
|
16759
|
+
opts.channel,
|
|
16760
|
+
opts.ts,
|
|
16761
|
+
formatPaneSnapshot({
|
|
16762
|
+
codeName: opts.codeName,
|
|
16763
|
+
text: last.body,
|
|
16764
|
+
source: last.source,
|
|
16765
|
+
capturedAtLabel: last.capturedAtLabel,
|
|
16766
|
+
status: { kind: "expired" },
|
|
16767
|
+
// ENG-6044: advertise the renamed command in the expired header.
|
|
16768
|
+
commandName: agentSlashCommand("/investigate")
|
|
16769
|
+
})
|
|
16770
|
+
);
|
|
16771
|
+
}
|
|
16772
|
+
}
|
|
16773
|
+
async function handleDebugSlashCommand(payload, responseUrl) {
|
|
16774
|
+
const codeName = AGENT_CODE_NAME ?? "unknown";
|
|
16775
|
+
const verdict = evaluateDebugGate({
|
|
16776
|
+
channelId: payload.channel_id,
|
|
16777
|
+
userId: payload.user_id,
|
|
16778
|
+
allowedUsers: ALLOWED_USERS
|
|
16779
|
+
});
|
|
16780
|
+
const investigateCmd = agentSlashCommand("/investigate");
|
|
16781
|
+
if (!verdict.ok) {
|
|
16782
|
+
debugAuditLog(`denied reason=${verdict.reason} user=${payload.user_id ?? "unknown"}`);
|
|
16783
|
+
const denialText = verdict.reason === "not-dm" ? `:warning: \`${investigateCmd}\` only works in a direct message with \`${codeName}\` \u2014 it shows the agent's raw terminal, so it stays 1:1. Open a DM with the bot and run \`${investigateCmd}\` there.` : verdict.reason === "allowlist-empty" ? `\u{1F6AB} \`${investigateCmd}\` is disabled for \`${codeName}\` \u2014 no diagnostic allowlist is configured. Because it exposes the agent's raw terminal, \`${investigateCmd}\` requires a non-empty \`SLACK_ALLOWED_USERS\` on the host (unlike the restart command, it does not open up when the allowlist is unset).` : `\u{1F6AB} \`${investigateCmd}\` denied \u2014 your Slack user isn't on the diagnostic allowlist for \`${codeName}\`. Ask whoever operates this host to add you to \`SLACK_ALLOWED_USERS\`.`;
|
|
16784
|
+
await postEphemeralViaResponseUrl(responseUrl, denialText, codeName);
|
|
16785
|
+
return;
|
|
16786
|
+
}
|
|
16787
|
+
const channelId = payload.channel_id;
|
|
16788
|
+
if (!channelId || !BOT_TOKEN) {
|
|
16789
|
+
debugAuditLog(
|
|
16790
|
+
`not-started reason=missing-${channelId ? "bot-token" : "channel"} user=${payload.user_id ?? "unknown"}`
|
|
16791
|
+
);
|
|
16792
|
+
await postEphemeralViaResponseUrl(
|
|
16793
|
+
responseUrl,
|
|
16794
|
+
`:warning: \`${investigateCmd}\` can't run \u2014 this channel process has no bot token wired.`,
|
|
16795
|
+
codeName
|
|
16796
|
+
);
|
|
16797
|
+
return;
|
|
16798
|
+
}
|
|
16799
|
+
if (activeDebugTailExpiresAtMs !== null && activeDebugTailExpiresAtMs > Date.now()) {
|
|
16800
|
+
const remaining = formatCountdown((activeDebugTailExpiresAtMs - Date.now()) / 1e3);
|
|
16801
|
+
debugAuditLog(
|
|
16802
|
+
`not-started reason=tail-already-running user=${payload.user_id ?? "unknown"} remaining=${remaining}`
|
|
16803
|
+
);
|
|
16804
|
+
await postEphemeralViaResponseUrl(
|
|
16805
|
+
responseUrl,
|
|
16806
|
+
`:hourglass: A \`${investigateCmd}\` tail is already running for \`${codeName}\` (one at a time) \u2014 it expires in ${remaining}.`,
|
|
16807
|
+
codeName
|
|
16808
|
+
);
|
|
16809
|
+
return;
|
|
16810
|
+
}
|
|
16811
|
+
const reservedExpiresAtMs = Date.now() + DEBUG_TAIL_WINDOW_MS;
|
|
16812
|
+
activeDebugTailExpiresAtMs = reservedExpiresAtMs;
|
|
16813
|
+
const snapshot = await capturePaneSnapshot({ codeName, agentDir: SLACK_AGENT_DIR });
|
|
16814
|
+
if (!snapshot) {
|
|
16815
|
+
activeDebugTailExpiresAtMs = null;
|
|
16816
|
+
debugAuditLog(`not-started reason=no-pane-output user=${payload.user_id ?? "unknown"}`);
|
|
16817
|
+
await postEphemeralViaResponseUrl(
|
|
16818
|
+
responseUrl,
|
|
16819
|
+
`:warning: No pane output available for \`${codeName}\` yet \u2014 the agent session may not have started. (If the whole host is wedged, \`${investigateCmd}\` can't reach it either \u2014 fall back to the SSM diagnostics runbook.)`,
|
|
16820
|
+
codeName
|
|
16821
|
+
);
|
|
16822
|
+
return;
|
|
16823
|
+
}
|
|
16824
|
+
debugAuditLog(`granted user=${payload.user_id ?? "unknown"} \u2014 starting live tail (source=${snapshot.source})`);
|
|
16825
|
+
const initialFrame = {
|
|
16826
|
+
body: redactPaneText(snapshot.text),
|
|
16827
|
+
source: snapshot.source,
|
|
16828
|
+
capturedAtLabel: nowUtcLabel()
|
|
16829
|
+
};
|
|
16830
|
+
const posted = await postSlackMessageWithTs({
|
|
16831
|
+
channel: channelId,
|
|
16832
|
+
text: formatPaneSnapshot({
|
|
16833
|
+
codeName,
|
|
16834
|
+
text: initialFrame.body,
|
|
16835
|
+
source: initialFrame.source,
|
|
16836
|
+
capturedAtLabel: initialFrame.capturedAtLabel,
|
|
16837
|
+
status: {
|
|
16838
|
+
kind: "live",
|
|
16839
|
+
secondsRemaining: Math.max(0, Math.round((reservedExpiresAtMs - Date.now()) / 1e3))
|
|
16840
|
+
}
|
|
16841
|
+
})
|
|
16842
|
+
});
|
|
16843
|
+
if (!posted.ok || !posted.ts) {
|
|
16844
|
+
activeDebugTailExpiresAtMs = null;
|
|
16845
|
+
debugAuditLog(
|
|
16846
|
+
`not-started reason=post-failed error=${posted.error ?? "unknown"} user=${payload.user_id ?? "unknown"}`
|
|
16847
|
+
);
|
|
16848
|
+
await postEphemeralViaResponseUrl(
|
|
16849
|
+
responseUrl,
|
|
16850
|
+
`:x: Failed to post the pane snapshot${posted.error ? ` (${posted.error})` : ""}. Try again in a moment.`,
|
|
16851
|
+
codeName
|
|
16852
|
+
);
|
|
16853
|
+
return;
|
|
16854
|
+
}
|
|
16855
|
+
void runDebugTailLoop({
|
|
16856
|
+
channel: channelId,
|
|
16857
|
+
ts: posted.ts,
|
|
16858
|
+
codeName,
|
|
16859
|
+
initialFrame,
|
|
16860
|
+
expiresAtMs: reservedExpiresAtMs
|
|
16861
|
+
}).catch((err) => {
|
|
16862
|
+
activeDebugTailExpiresAtMs = null;
|
|
16863
|
+
debugAuditLog(`tail loop crashed: ${redactAugmentedPaths2(err.message)}`);
|
|
16864
|
+
});
|
|
16865
|
+
}
|
|
16077
16866
|
async function handleSlashCommandEnvelope(payload) {
|
|
16078
16867
|
const command = payload.command;
|
|
16079
16868
|
const responseUrl = payload.response_url;
|
|
16080
16869
|
const codeName = AGENT_CODE_NAME ?? "unknown";
|
|
16081
16870
|
if (!command || !responseUrl) return;
|
|
16082
|
-
if (command
|
|
16871
|
+
if (matchesAgentCommand(command, "/agent-status")) {
|
|
16083
16872
|
await postEphemeralViaResponseUrl(responseUrl, buildAgentStatusReply(codeName), codeName);
|
|
16084
16873
|
return;
|
|
16085
16874
|
}
|
|
@@ -16087,7 +16876,7 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16087
16876
|
await postEphemeralViaResponseUrl(responseUrl, buildSlackHelpMessage(codeName), codeName);
|
|
16088
16877
|
return;
|
|
16089
16878
|
}
|
|
16090
|
-
if (command
|
|
16879
|
+
if (matchesAgentCommand(command, "/restart")) {
|
|
16091
16880
|
if (ALLOWED_USERS.size > 0 && (!payload.user_id || !ALLOWED_USERS.has(payload.user_id))) {
|
|
16092
16881
|
process.stderr.write(
|
|
16093
16882
|
`slack-channel(${codeName}): /restart slash-command denied \u2014 user not in SLACK_ALLOWED_USERS
|
|
@@ -16109,10 +16898,10 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16109
16898
|
return;
|
|
16110
16899
|
}
|
|
16111
16900
|
try {
|
|
16112
|
-
if (!
|
|
16901
|
+
if (!existsSync3(RESTART_FLAGS_DIR)) {
|
|
16113
16902
|
mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
|
|
16114
16903
|
}
|
|
16115
|
-
const flagPath =
|
|
16904
|
+
const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
16116
16905
|
const flag = {
|
|
16117
16906
|
codeName,
|
|
16118
16907
|
source: "slack",
|
|
@@ -16136,7 +16925,7 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16136
16925
|
);
|
|
16137
16926
|
} catch (err) {
|
|
16138
16927
|
process.stderr.write(
|
|
16139
|
-
`slack-channel(${codeName}): /restart slash-command flag write failed: ${
|
|
16928
|
+
`slack-channel(${codeName}): /restart slash-command flag write failed: ${redactAugmentedPaths2(err.message)}
|
|
16140
16929
|
`
|
|
16141
16930
|
);
|
|
16142
16931
|
await postEphemeralViaResponseUrl(
|
|
@@ -16147,6 +16936,10 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16147
16936
|
}
|
|
16148
16937
|
return;
|
|
16149
16938
|
}
|
|
16939
|
+
if (command === "/debug" || matchesAgentCommand(command, "/investigate")) {
|
|
16940
|
+
await handleDebugSlashCommand(payload, responseUrl);
|
|
16941
|
+
return;
|
|
16942
|
+
}
|
|
16150
16943
|
if (command === "/kill" || command === "/unkill") {
|
|
16151
16944
|
if (!AGT_HOST || !AGT_API_KEY || !AGT_AGENT_ID) {
|
|
16152
16945
|
await postEphemeralViaResponseUrl(
|
|
@@ -16216,10 +17009,10 @@ async function handleHelpCommand(opts) {
|
|
|
16216
17009
|
async function handleRestartCommand(opts) {
|
|
16217
17010
|
const codeName = AGENT_CODE_NAME ?? "unknown";
|
|
16218
17011
|
try {
|
|
16219
|
-
if (!
|
|
17012
|
+
if (!existsSync3(RESTART_FLAGS_DIR)) {
|
|
16220
17013
|
mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
|
|
16221
17014
|
}
|
|
16222
|
-
const flagPath =
|
|
17015
|
+
const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
16223
17016
|
const flag = {
|
|
16224
17017
|
codeName,
|
|
16225
17018
|
source: "slack",
|
|
@@ -16250,7 +17043,7 @@ async function handleRestartCommand(opts) {
|
|
|
16250
17043
|
}
|
|
16251
17044
|
} catch (err) {
|
|
16252
17045
|
process.stderr.write(
|
|
16253
|
-
`slack-channel(${codeName}): /restart flag write failed: ${
|
|
17046
|
+
`slack-channel(${codeName}): /restart flag write failed: ${redactAugmentedPaths2(err.message)}
|
|
16254
17047
|
`
|
|
16255
17048
|
);
|
|
16256
17049
|
await postSlackMessage({
|
|
@@ -16278,7 +17071,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
|
|
|
16278
17071
|
var threadPersister = null;
|
|
16279
17072
|
function resolveThreadStorePath() {
|
|
16280
17073
|
if (!AGENT_CODE_NAME) return null;
|
|
16281
|
-
return
|
|
17074
|
+
return join5(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
|
|
16282
17075
|
}
|
|
16283
17076
|
function parseTtlDays(raw) {
|
|
16284
17077
|
if (!raw) return void 0;
|
|
@@ -16313,9 +17106,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
|
|
|
16313
17106
|
var slackStderrLogStream = null;
|
|
16314
17107
|
if (AGENT_CODE_NAME) {
|
|
16315
17108
|
try {
|
|
16316
|
-
const logDir =
|
|
17109
|
+
const logDir = join5(homedir2(), ".augmented", AGENT_CODE_NAME);
|
|
16317
17110
|
mkdirSync3(logDir, { recursive: true });
|
|
16318
|
-
slackStderrLogStream = createWriteStream(
|
|
17111
|
+
slackStderrLogStream = createWriteStream(join5(logDir, "slack-channel-stderr.log"), {
|
|
16319
17112
|
flags: "a",
|
|
16320
17113
|
mode: 384
|
|
16321
17114
|
});
|
|
@@ -16399,11 +17192,11 @@ var mcp = new Server(
|
|
|
16399
17192
|
// Highest-priority lines first — Claude Code truncates this string at
|
|
16400
17193
|
// 2048 chars, so anything appended late silently disappears.
|
|
16401
17194
|
"CRITICAL: every response to a Slack <channel> tag MUST go through slack.reply. Text in your session WITHOUT a slack.reply call never reaches the user \u2014 the message dies inside the agent process.",
|
|
16402
|
-
`Inbound: <channel ... thread_ts="..." [thread_context="..."]>. Pass channel +
|
|
17195
|
+
`Inbound: <channel ... thread_ts="..." message_ts="..." [thread_context="..."]>. Pass channel + message_ts to slack.reply (and thread_ts on threads). thread_context = thread pre-loaded; ground replies ONLY in it, never another channel's.`,
|
|
16403
17196
|
"Inbound attachments: <channel> `files` is a JSON-serialised array \u2014 JSON.parse it. If an entry has `path`, the image is already downloaded \u2014 Read it directly, do NOT call slack.download_attachment. Use that tool only for entries with `file_id` but NO `path` (PDF, docx, csv): pass file_id + channel verbatim, then Read the returned path. Single-image messages also get a top-level `image_path`. Don't surface internal file-handling errors that don't affect the answer.",
|
|
16404
17197
|
"Address users by user_name, never by raw user ID. In multi-participant threads the CURRENT speaker is the one on the latest <channel> tag.",
|
|
16405
17198
|
'Mentioned in a channel \u2192 respond in that thread. DM \u2192 respond directly. auto_followed="true" \u2192 only reply if useful, OR if your own bot user is @-mentioned (counts even in auto_followed).',
|
|
16406
|
-
"Reaction taxonomy (use slack.react sparingly \u2014 prefer a reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u2705 = success. NEVER react to signal failure of YOUR work \u2014 users can't tell why something failed from an emoji. On failure, slack.reply with one sentence explaining what went wrong (no stack traces, no secrets). (The \
|
|
17199
|
+
"Reaction taxonomy (use slack.react sparingly \u2014 prefer a reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u2705 = success. NEVER react to signal failure of YOUR work \u2014 users can't tell why something failed from an emoji. On failure, slack.reply with one sentence explaining what went wrong (no stack traces, no secrets). (The \u23F3 you may see on an inbound is applied by the system, not you \u2014 it marks a message that arrived while the agent couldn't reply; never add \u23F3 or \u274C yourself.)",
|
|
16407
17200
|
`When a thread message is NOT addressed to you (different @-mention, side conversation, auto_followed catch-up): SILENTLY SKIP \u2014 no reaction, no reply, no "this wasn't for me" message.`,
|
|
16408
17201
|
"To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
|
|
16409
17202
|
].join(" ")
|
|
@@ -16424,7 +17217,18 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16424
17217
|
text: { type: "string", description: "The message to send" },
|
|
16425
17218
|
thread_ts: {
|
|
16426
17219
|
type: "string",
|
|
16427
|
-
description: "Thread timestamp for threaded replies (from the thread_ts attribute)"
|
|
17220
|
+
description: "Thread timestamp for threaded replies (from the thread_ts attribute). Omit for a top-level reply (e.g. a fresh DM)."
|
|
17221
|
+
},
|
|
17222
|
+
// ENG-5861: explicit message_ts so the cleanup gate can match the
|
|
17223
|
+
// exact pending-inbound marker — necessary because top-level DM
|
|
17224
|
+
// replies legitimately have no thread_ts and the marker filename
|
|
17225
|
+
// is `<channel>__<thread_ts>__<message_ts>.json`. Without this
|
|
17226
|
+
// the marker sat undrained, eventually tripping ENG-5832\'s
|
|
17227
|
+
// "wedged" threshold and firing a false-positive red ✗ on the
|
|
17228
|
+
// next inbound.
|
|
17229
|
+
message_ts: {
|
|
17230
|
+
type: "string",
|
|
17231
|
+
description: "The message_ts of the specific inbound this reply addresses (from the message_ts attribute on the <channel> tag). Used by the pending-inbound cleanup gate so the marker is reliably cleared even for top-level DMs that have no thread_ts."
|
|
16428
17232
|
}
|
|
16429
17233
|
},
|
|
16430
17234
|
required: ["channel", "text"]
|
|
@@ -16432,7 +17236,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16432
17236
|
},
|
|
16433
17237
|
{
|
|
16434
17238
|
name: "slack.react",
|
|
16435
|
-
description: "Add an emoji reaction to a Slack message. Use sparingly \u2014 prefer a text reply. Reaction taxonomy: \u2705 = action completed successfully. NEVER react to signal failure of your work \u2014 users can't tell why it failed from a reaction. On failure, call slack.reply with one sentence explaining what went wrong instead. \u{1F440} (eyes) is already auto-applied on inbound; do not duplicate. \
|
|
17239
|
+
description: "Add an emoji reaction to a Slack message. Use sparingly \u2014 prefer a text reply. Reaction taxonomy: \u2705 = action completed successfully. NEVER react to signal failure of your work \u2014 users can't tell why it failed from a reaction. On failure, call slack.reply with one sentence explaining what went wrong instead. \u{1F440} (eyes) is already auto-applied on inbound; do not duplicate. \u23F3 (:hourglass_flowing_sand:) is reserved for the system to mark an inbound that arrived while the agent couldn't reply \u2014 never add \u23F3 or \u274C yourself.",
|
|
16436
17240
|
inputSchema: {
|
|
16437
17241
|
type: "object",
|
|
16438
17242
|
properties: {
|
|
@@ -16490,7 +17294,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16490
17294
|
...blockKitToolsAvailable() ? [
|
|
16491
17295
|
{
|
|
16492
17296
|
name: "slack.send_structured",
|
|
16493
|
-
description:
|
|
17297
|
+
description: "Send a Slack message built from Block Kit blocks (structured layout: headers, sections, dividers, context, action buttons). Use when plain text isn't enough \u2014 status reports, confirmations with multiple fields, calls-to-action with buttons. Pass a `text` fallback for push notifications. Returns { message_ts, permalink }. Does NOT block waiting for user interactions; for that use slack.ask_user.",
|
|
16494
17298
|
inputSchema: {
|
|
16495
17299
|
type: "object",
|
|
16496
17300
|
properties: {
|
|
@@ -16500,18 +17304,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16500
17304
|
description: "Array of Block Kit blocks. Supported block types: header, section, divider, context, actions. Supported interactive elements (inside actions blocks): button. Hard limits: 50 blocks per message, 5 elements per actions block, 3000 chars per section text, 75 chars per button label."
|
|
16501
17305
|
},
|
|
16502
17306
|
text: { type: "string", description: "Plain-text fallback for push notifications and unfurls. Required." },
|
|
16503
|
-
thread_ts: { type: "string", description: "Thread timestamp for threaded replies (optional)" }
|
|
16504
|
-
interactive: {
|
|
16505
|
-
type: "object",
|
|
16506
|
-
description: `Optional interactive affordance appended to the message. Currently supports type="rate_run" \u2014 appends a 1-5 button row that updates this run's rating when tapped. Use after a scheduled-task delivery so users can rate the firing.`,
|
|
16507
|
-
properties: {
|
|
16508
|
-
type: { type: "string", enum: ["rate_run"], description: 'Affordance kind. Only "rate_run" is supported today.' },
|
|
16509
|
-
run_id: { type: "string", description: "The runs.run_id this rating belongs to (returned by /host/runs/start)." },
|
|
16510
|
-
scale: { type: "string", enum: ["1-5", "sentiment"], description: 'Rating scale. Default "1-5" (1..5 buttons). "sentiment" emits \u{1F44E}/\u{1F914}/\u{1F44D} mapping to -1/0/+1 \u2014 reserved for future Telegram parity.' },
|
|
16511
|
-
expires_in_seconds: { type: "number", description: "How long the rating row stays tappable. Default 86400 (24h). The pending row times out after this; later taps are ignored." }
|
|
16512
|
-
},
|
|
16513
|
-
required: ["type", "run_id"]
|
|
16514
|
-
}
|
|
17307
|
+
thread_ts: { type: "string", description: "Thread timestamp for threaded replies (optional)" }
|
|
16515
17308
|
},
|
|
16516
17309
|
required: ["channel", "blocks", "text"]
|
|
16517
17310
|
}
|
|
@@ -16662,7 +17455,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
16662
17455
|
return buildImpersonationRefusal(name);
|
|
16663
17456
|
}
|
|
16664
17457
|
if (name === "slack.reply") {
|
|
16665
|
-
const { channel, text, thread_ts } = args;
|
|
17458
|
+
const { channel, text, thread_ts, message_ts } = args;
|
|
16666
17459
|
if (channel && thread_ts) {
|
|
16667
17460
|
const killed = await isThreadKilled({
|
|
16668
17461
|
channelType: "slack",
|
|
@@ -16721,8 +17514,15 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
16721
17514
|
isError: true
|
|
16722
17515
|
};
|
|
16723
17516
|
}
|
|
16724
|
-
if (channel
|
|
16725
|
-
|
|
17517
|
+
if (channel) {
|
|
17518
|
+
if (message_ts) {
|
|
17519
|
+
clearSlackPendingMarkerByMessageTs2(channel, message_ts);
|
|
17520
|
+
if (thread_ts) clearPendingMessage(channel, thread_ts);
|
|
17521
|
+
} else if (thread_ts) {
|
|
17522
|
+
clearPendingMessage(channel, thread_ts);
|
|
17523
|
+
} else {
|
|
17524
|
+
clearOldestSlackPendingMarkerInChannel2(channel);
|
|
17525
|
+
}
|
|
16726
17526
|
}
|
|
16727
17527
|
try {
|
|
16728
17528
|
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
@@ -16874,14 +17674,14 @@ ${result.formatted}` : "Thread is empty or not found."
|
|
|
16874
17674
|
let bytes;
|
|
16875
17675
|
let size;
|
|
16876
17676
|
try {
|
|
16877
|
-
const
|
|
16878
|
-
if (!
|
|
17677
|
+
const stat2 = statSync2(resolvedPath);
|
|
17678
|
+
if (!stat2.isFile()) {
|
|
16879
17679
|
return {
|
|
16880
17680
|
content: [{ type: "text", text: `Upload refused: ${resolvedPath} is not a regular file.` }],
|
|
16881
17681
|
isError: true
|
|
16882
17682
|
};
|
|
16883
17683
|
}
|
|
16884
|
-
size =
|
|
17684
|
+
size = stat2.size;
|
|
16885
17685
|
bytes = readFileSync4(resolvedPath);
|
|
16886
17686
|
} catch (err) {
|
|
16887
17687
|
return {
|
|
@@ -16992,7 +17792,7 @@ ${result.formatted}` : "Thread is empty or not found."
|
|
|
16992
17792
|
return {
|
|
16993
17793
|
content: [{
|
|
16994
17794
|
type: "text",
|
|
16995
|
-
text: `Failed to download attachment: ${
|
|
17795
|
+
text: `Failed to download attachment: ${redactAugmentedPaths2(err.message)}`
|
|
16996
17796
|
}],
|
|
16997
17797
|
isError: true
|
|
16998
17798
|
};
|
|
@@ -17179,7 +17979,7 @@ async function handleSendStructured(args) {
|
|
|
17179
17979
|
}
|
|
17180
17980
|
const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
|
|
17181
17981
|
const { validateSlackBlocks: validateSlackBlocks2 } = runtime;
|
|
17182
|
-
const { channel, blocks, text, thread_ts
|
|
17982
|
+
const { channel, blocks, text, thread_ts } = args;
|
|
17183
17983
|
if (typeof channel !== "string" || !channel) {
|
|
17184
17984
|
return errResult("channel is required");
|
|
17185
17985
|
}
|
|
@@ -17191,97 +17991,16 @@ async function handleSendStructured(args) {
|
|
|
17191
17991
|
if (!validation.ok) {
|
|
17192
17992
|
return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
17193
17993
|
}
|
|
17194
|
-
let effectiveBlocks = blocks;
|
|
17195
|
-
let ratingCallbackId = null;
|
|
17196
|
-
let ratingExpiresAt = null;
|
|
17197
|
-
let ratingTokenised = [];
|
|
17198
|
-
if (interactive) {
|
|
17199
|
-
if (interactive.type !== "rate_run") {
|
|
17200
|
-
return errResult(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
|
|
17201
|
-
}
|
|
17202
|
-
if (typeof interactive.run_id !== "string" || !interactive.run_id) {
|
|
17203
|
-
return errResult("interactive.run_id is required when interactive is set");
|
|
17204
|
-
}
|
|
17205
|
-
if (!interactiveHostAvailable()) {
|
|
17206
|
-
return errResult(
|
|
17207
|
-
"interactive=rate_run requires Block Kit plus host MCP wiring (AGT_HOST, AGT_API_KEY, AGT_AGENT_ID)."
|
|
17208
|
-
);
|
|
17209
|
-
}
|
|
17210
|
-
const scale = interactive.scale === "sentiment" ? "sentiment" : "1-5";
|
|
17211
|
-
const requestedTtl = typeof interactive.expires_in_seconds === "number" ? interactive.expires_in_seconds : 86400;
|
|
17212
|
-
const ttl = Math.max(60, Math.min(7 * 86400, requestedTtl));
|
|
17213
|
-
const { randomUUID: rndUUID } = await import("crypto");
|
|
17214
|
-
ratingCallbackId = rndUUID();
|
|
17215
|
-
ratingExpiresAt = new Date(Date.now() + ttl * 1e3);
|
|
17216
|
-
const values = runtime.ratingValuesForScale(scale);
|
|
17217
|
-
const labels = runtime.defaultRatingLabels(scale);
|
|
17218
|
-
ratingTokenised = values.map((v, i) => ({
|
|
17219
|
-
token: runtime.generateOptionToken(),
|
|
17220
|
-
value: String(v),
|
|
17221
|
-
label: labels[i] ?? String(v)
|
|
17222
|
-
}));
|
|
17223
|
-
const ratingBlock = runtime.buildRatingActionsBlock({
|
|
17224
|
-
callbackId: ratingCallbackId,
|
|
17225
|
-
options: ratingTokenised.map(({ label, token }) => ({
|
|
17226
|
-
// The numeric value is carried by token → options mapping in the
|
|
17227
|
-
// pending_interaction row, not by the button itself, so this
|
|
17228
|
-
// cast-back to RatingOption only needs label + token + a placeholder.
|
|
17229
|
-
value: 0,
|
|
17230
|
-
label,
|
|
17231
|
-
token
|
|
17232
|
-
}))
|
|
17233
|
-
});
|
|
17234
|
-
effectiveBlocks = [...blocks, ratingBlock];
|
|
17235
|
-
const finalValidation = runtime.validateSlackBlocks(effectiveBlocks);
|
|
17236
|
-
if (!finalValidation.ok) {
|
|
17237
|
-
return errResult(
|
|
17238
|
-
`Invalid blocks (after appending rating row): ${finalValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
|
|
17239
|
-
);
|
|
17240
|
-
}
|
|
17241
|
-
const cfg = {
|
|
17242
|
-
apiHost: AGT_HOST,
|
|
17243
|
-
apiKey: AGT_API_KEY,
|
|
17244
|
-
agentId: AGT_AGENT_ID
|
|
17245
|
-
};
|
|
17246
|
-
try {
|
|
17247
|
-
await runtime.createPendingInteraction(cfg, {
|
|
17248
|
-
callbackId: ratingCallbackId,
|
|
17249
|
-
channelId: channel,
|
|
17250
|
-
messageTs: "",
|
|
17251
|
-
threadTs: thread_ts,
|
|
17252
|
-
options: ratingTokenised,
|
|
17253
|
-
expiresAt: ratingExpiresAt,
|
|
17254
|
-
kind: "rate_run",
|
|
17255
|
-
kindData: { run_id: interactive.run_id, scale }
|
|
17256
|
-
});
|
|
17257
|
-
} catch (err) {
|
|
17258
|
-
return errResult(`Failed to register rating row: ${err.message}`);
|
|
17259
|
-
}
|
|
17260
|
-
}
|
|
17261
17994
|
const result = await postSlackMessageWithTs({
|
|
17262
17995
|
channel,
|
|
17263
|
-
blocks
|
|
17996
|
+
blocks,
|
|
17264
17997
|
text,
|
|
17265
17998
|
...thread_ts ? { thread_ts } : {}
|
|
17266
17999
|
});
|
|
17267
18000
|
if (!result.ok || !result.ts) {
|
|
17268
18001
|
return errResult(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
|
|
17269
18002
|
}
|
|
17270
|
-
if (
|
|
17271
|
-
try {
|
|
17272
|
-
await runtime.updatePendingInteractionMessageTs(
|
|
17273
|
-
{ apiHost: AGT_HOST, apiKey: AGT_API_KEY, agentId: AGT_AGENT_ID },
|
|
17274
|
-
ratingCallbackId,
|
|
17275
|
-
result.ts
|
|
17276
|
-
);
|
|
17277
|
-
} catch (err) {
|
|
17278
|
-
process.stderr.write(
|
|
17279
|
-
`slack-channel(${AGENT_CODE_NAME}): updatePendingInteractionMessageTs failed for rating ${hashId(ratingCallbackId)}: ${err.message}
|
|
17280
|
-
`
|
|
17281
|
-
);
|
|
17282
|
-
}
|
|
17283
|
-
}
|
|
17284
|
-
if (interactiveHostAvailable() && !ratingCallbackId) {
|
|
18003
|
+
if (interactiveHostAvailable()) {
|
|
17285
18004
|
const cfg = {
|
|
17286
18005
|
apiHost: AGT_HOST,
|
|
17287
18006
|
apiKey: AGT_API_KEY,
|
|
@@ -17311,8 +18030,7 @@ async function handleSendStructured(args) {
|
|
|
17311
18030
|
type: "text",
|
|
17312
18031
|
text: JSON.stringify({
|
|
17313
18032
|
message_ts: result.ts,
|
|
17314
|
-
permalink: permalink ?? ""
|
|
17315
|
-
...ratingCallbackId ? { rating_callback_id: ratingCallbackId } : {}
|
|
18033
|
+
permalink: permalink ?? ""
|
|
17316
18034
|
})
|
|
17317
18035
|
}
|
|
17318
18036
|
]
|
|
@@ -17549,7 +18267,7 @@ function isDownloadableFileId(fileId, channel) {
|
|
|
17549
18267
|
}
|
|
17550
18268
|
return entry.channel === channel;
|
|
17551
18269
|
}
|
|
17552
|
-
function
|
|
18270
|
+
function redactAugmentedPaths2(msg) {
|
|
17553
18271
|
return msg.replaceAll(
|
|
17554
18272
|
new RegExp(`${homedir2().replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}/\\.augmented/[^\\s'"\`]*`, "g"),
|
|
17555
18273
|
"<augmented-path>"
|
|
@@ -17631,7 +18349,7 @@ async function buildInboundFileMeta(rawFiles, codeName, channel) {
|
|
|
17631
18349
|
});
|
|
17632
18350
|
} catch (err) {
|
|
17633
18351
|
process.stderr.write(
|
|
17634
|
-
`slack-channel: image auto-download failed for ${classified.id}: ${
|
|
18352
|
+
`slack-channel: image auto-download failed for ${classified.id}: ${redactAugmentedPaths2(err.message)}
|
|
17635
18353
|
`
|
|
17636
18354
|
);
|
|
17637
18355
|
registerDownloadableFileId(classified.id, channel);
|
|
@@ -17714,6 +18432,7 @@ async function connectSocketMode() {
|
|
|
17714
18432
|
process.stderr.write("slack-channel: Socket Mode connected\n");
|
|
17715
18433
|
recordActivity("connect");
|
|
17716
18434
|
void setBotStatus(":large_green_circle:", "Active");
|
|
18435
|
+
void notifyStrandedInboundsOnFirstConnect();
|
|
17717
18436
|
};
|
|
17718
18437
|
ws.onmessage = async (event) => {
|
|
17719
18438
|
try {
|
|
@@ -17775,8 +18494,27 @@ async function connectSocketMode() {
|
|
|
17775
18494
|
}
|
|
17776
18495
|
const senderPolicyDecision = decideSenderPolicyForward(evt, SLACK_SENDER_POLICY);
|
|
17777
18496
|
if (!senderPolicyDecision.forward) {
|
|
17778
|
-
|
|
18497
|
+
const subReason = classifySlackPolicyBlock(evt, SLACK_SENDER_POLICY);
|
|
18498
|
+
process.stderr.write(`slack-channel: dropped message event (reason=sender_policy, sub=${subReason}, mode=${SLACK_SENDER_POLICY.mode}, ts=${evt.ts ?? "n/a"})
|
|
17779
18499
|
`);
|
|
18500
|
+
await maybeSendSenderPolicyDecline({
|
|
18501
|
+
channel: evt.channel,
|
|
18502
|
+
senderId: evt.user,
|
|
18503
|
+
// CR on PR #1623: top-level posts (DMs, channel root messages)
|
|
18504
|
+
// arrive with no `thread_ts` and MUST decline inline, not in a
|
|
18505
|
+
// new thread off the inbound message. The `?? evt.ts` fallback
|
|
18506
|
+
// forced every root message into a thread reply — exactly the
|
|
18507
|
+
// case the helper's "omit thread_ts when undefined" branch was
|
|
18508
|
+
// meant to handle. Pass through as-is: in-thread for thread
|
|
18509
|
+
// replies (thread_ts present), inline for everything else.
|
|
18510
|
+
threadTs: evt.thread_ts,
|
|
18511
|
+
subReason
|
|
18512
|
+
}).catch((err) => {
|
|
18513
|
+
process.stderr.write(
|
|
18514
|
+
`slack-channel(${AGENT_CODE_NAME}): decline reply failed: ${err.message}
|
|
18515
|
+
`
|
|
18516
|
+
);
|
|
18517
|
+
});
|
|
17780
18518
|
return;
|
|
17781
18519
|
}
|
|
17782
18520
|
recordActivity("inbound");
|
|
@@ -17786,7 +18524,8 @@ async function connectSocketMode() {
|
|
|
17786
18524
|
const threadKey = buildThreadKey(evt.channel, trackTs);
|
|
17787
18525
|
const rawText = evt.text ?? "";
|
|
17788
18526
|
const strippedText = rawText.replace(/^\s*<@[^>]+>\s*/, "").trim();
|
|
17789
|
-
const
|
|
18527
|
+
const restartSuffixed = agentSlashCommand("/restart");
|
|
18528
|
+
const isRestartCommand = strippedText === "/restart" || strippedText.startsWith("/restart ") || strippedText === restartSuffixed || strippedText.startsWith(`${restartSuffixed} `);
|
|
17790
18529
|
const isHelpCommand = strippedText === "/help" || strippedText.startsWith("/help ");
|
|
17791
18530
|
if (isHelpCommand) {
|
|
17792
18531
|
await handleHelpCommand({
|
|
@@ -17903,16 +18642,25 @@ async function connectSocketMode() {
|
|
|
17903
18642
|
botUserId
|
|
17904
18643
|
});
|
|
17905
18644
|
const ackProbe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
|
|
18645
|
+
let paneLogFreshAgeMs = null;
|
|
18646
|
+
if (SLACK_AGENT_DIR) {
|
|
18647
|
+
try {
|
|
18648
|
+
const paneMtimeMs = statSync2(join5(SLACK_AGENT_DIR, "pane.log")).mtimeMs;
|
|
18649
|
+
paneLogFreshAgeMs = Math.max(0, Date.now() - paneMtimeMs);
|
|
18650
|
+
} catch {
|
|
18651
|
+
}
|
|
18652
|
+
}
|
|
17906
18653
|
const ackDecision = decideAckReaction({
|
|
17907
18654
|
hasTarget: decideSlackAckReaction({ channel, ts }),
|
|
17908
18655
|
integrationReady: Boolean(BOT_TOKEN),
|
|
17909
18656
|
tmux: ackProbe.tmux,
|
|
17910
18657
|
claude: ackProbe.claude,
|
|
17911
18658
|
withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
|
|
17912
|
-
oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR)
|
|
18659
|
+
oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR),
|
|
18660
|
+
paneLogFreshAgeMs
|
|
17913
18661
|
});
|
|
17914
18662
|
if (ackDecision !== "none") {
|
|
17915
|
-
const reactionName = ackDecision === "undeliverable" ?
|
|
18663
|
+
const reactionName = ackDecision === "undeliverable" ? SLACK_UNDELIVERABLE_REACTION : "eyes";
|
|
17916
18664
|
fetch("https://slack.com/api/reactions.add", {
|
|
17917
18665
|
method: "POST",
|
|
17918
18666
|
headers: {
|
|
@@ -17922,6 +18670,9 @@ async function connectSocketMode() {
|
|
|
17922
18670
|
body: JSON.stringify({ channel, timestamp: ts, name: reactionName })
|
|
17923
18671
|
}).catch(() => {
|
|
17924
18672
|
});
|
|
18673
|
+
if (ackDecision === "undeliverable" && channel && shouldEngage) {
|
|
18674
|
+
postUndeliverableNotice(channel, threadTs, isThreadReply);
|
|
18675
|
+
}
|
|
17925
18676
|
}
|
|
17926
18677
|
if (channel && ts && shouldEngage) {
|
|
17927
18678
|
trackPendingMessage(channel, threadTs, ts, ackDecision === "undeliverable");
|
|
@@ -17964,6 +18715,13 @@ async function connectSocketMode() {
|
|
|
17964
18715
|
user_name: userName,
|
|
17965
18716
|
channel,
|
|
17966
18717
|
thread_ts: threadTs,
|
|
18718
|
+
// ENG-5861: explicit message_ts so the agent can pass it back
|
|
18719
|
+
// to slack.reply for reliable pending-inbound cleanup. For
|
|
18720
|
+
// top-level messages threadTs === ts; for threaded replies
|
|
18721
|
+
// threadTs is the thread root and message_ts is the specific
|
|
18722
|
+
// reply being addressed — different keys, both needed by the
|
|
18723
|
+
// marker-cleanup gate.
|
|
18724
|
+
message_ts: ts,
|
|
17967
18725
|
event_type: evt.type,
|
|
17968
18726
|
...isAutoFollowed ? { auto_followed: "true" } : {},
|
|
17969
18727
|
// Only set these when we actually have attachments to avoid
|
|
@@ -18084,3 +18842,7 @@ if (THREAD_STORE_PATH) {
|
|
|
18084
18842
|
);
|
|
18085
18843
|
}
|
|
18086
18844
|
connectSocketModeSafely();
|
|
18845
|
+
export {
|
|
18846
|
+
__resetSlackGiveUpNoticeStateForTests,
|
|
18847
|
+
__resetSlackUndeliverableNoticeThrottle
|
|
18848
|
+
};
|